PerformanceInteractionMetrics.cpp (9675B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "PerformanceInteractionMetrics.h" 8 9 #include "mozilla/EventForwards.h" 10 #include "mozilla/Maybe.h" 11 #include "mozilla/MouseEvents.h" 12 #include "mozilla/RandomNum.h" 13 #include "mozilla/TextEvents.h" 14 15 // Interaction ID increment. We increase this value by an integer greater than 1 16 // to discourage developers from using the value to 'count' the number of user 17 // interactions. This is consistent with the spec, which allows the increasing 18 // the user interaction value by a small number chosen by the user agent. 19 constexpr uint32_t kInteractionIdIncrement = 7; 20 // Minimum potential value for the first Interaction ID. 21 constexpr uint32_t kMinFirstInteractionID = 100; 22 // Maximum potential value for the first Interaction ID. 23 constexpr uint32_t kMaxFirstInteractionID = 10000; 24 25 constexpr uint32_t kNonPointerId = -1; 26 27 namespace mozilla::dom { 28 29 PerformanceInteractionMetrics::PerformanceInteractionMetrics() { 30 uint64_t randVal = RandomUint64().valueOr(kMinFirstInteractionID); 31 // Choose a random integer as the initial value to discourage developers from 32 // using interactionId to counter the number of interactions. 33 // https://wicg.github.io/event-timing/#user-interaction-value 34 mCurrentInteractionValue = 35 kMinFirstInteractionID + 36 (randVal % (kMaxFirstInteractionID - kMinFirstInteractionID + 1)); 37 } 38 39 // https://w3c.github.io/event-timing/#sec-increasing-interaction-count 40 uint64_t PerformanceInteractionMetrics::IncreaseInteractionValueAndCount() { 41 mCurrentInteractionValue += kInteractionIdIncrement; 42 mInteractionCount++; 43 return mCurrentInteractionValue; 44 } 45 46 // https://w3c.github.io/event-timing/#sec-computing-interactionid 47 Maybe<uint64_t> PerformanceInteractionMetrics::ComputeInteractionId( 48 PerformanceEventTiming* aEventTiming, const WidgetEvent* aEvent) { 49 // Step 1. If event’s isTrusted attribute value is false, return 0. 50 if (!aEvent->IsTrusted()) { 51 return Some(0); 52 } 53 54 // Step 2. Let type be event’s type attribute value. 55 const EventMessage eventType = aEvent->mMessage; 56 57 // Step 3. If type is not one among keyup, compositionstart, input, 58 // pointercancel, pointerup, or click, return 0. 59 // Note: keydown and pointerdown are handled in finalize event timing. 60 switch (eventType) { 61 case eKeyDown: 62 case eKeyPress: 63 case eKeyUp: 64 case eCompositionStart: 65 case eEditorInput: 66 case ePointerCancel: 67 case ePointerDown: 68 case ePointerUp: 69 case eContextMenu: 70 case ePointerClick: 71 break; 72 default: 73 return Some(0); 74 } 75 76 // Step 4-8. Happens in the class constructor. 77 78 if (eventType == ePointerDown) { 79 uint32_t pointerId = aEvent->AsPointerEvent()->pointerId; 80 81 // If after the previously buffered pointerdown we haven't encountered any 82 // matching pointerup, we need to set it as zero so it properly flushes the 83 // events. 84 auto bufferedPointerdown = mPendingPointerDowns.MaybeGet(pointerId); 85 if (NS_WARN_IF(bufferedPointerdown && 86 !(*bufferedPointerdown)->HasKnownInteractionId())) { 87 (*bufferedPointerdown)->SetInteractionId(0); 88 } 89 mPendingPointerDowns.InsertOrUpdate(pointerId, aEventTiming); 90 mContextMenuTriggered = false; 91 // InteractionId for this will be assigned by pointerup or pointercancel 92 // later. 93 return Nothing(); 94 } 95 96 if (eventType == eKeyDown) { 97 const WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); 98 99 if (keyEvent->mIsComposing) { 100 return Some(0); 101 } 102 103 auto code = keyEvent->mKeyCode; 104 105 // This is not part of the spec yet, but it's being discussed and will be 106 // added to the spec soon. 107 // See: https://github.com/w3c/event-timing/issues/153 108 mPendingKeyDowns.InsertOrUpdate(code, aEventTiming); 109 uint64_t interactionId = IncreaseInteractionValueAndCount(); 110 mLastKeydownInteractionValue = Some(interactionId); 111 return Some(interactionId); 112 } 113 114 // keypress is not a part of the spec yet, but it's being worked on: 115 // https://github.com/w3c/event-timing/issues/149 116 if (eventType == eKeyPress) { 117 // Set the interaction id generated by the previous keydown entry. 118 return Some(mCurrentInteractionValue); 119 } 120 121 // Step 8. If type is keyup: 122 if (eventType == eKeyUp) { 123 // Step 8.1. If event’s isComposing attribute value is true, return 0. 124 const WidgetKeyboardEvent* keyEvent = aEvent->AsKeyboardEvent(); 125 if (keyEvent->mIsComposing) { 126 return Some(0); 127 } 128 129 // Step 8.2. Let code be event’s keyCode attribute value. 130 const uint32_t code = keyEvent->mKeyCode; 131 132 // Step 8.4. Let entry be pendingKeyDowns[code]. 133 auto entry = mPendingKeyDowns.MaybeGet(code); 134 // Step 8.3. If pendingKeyDowns[code] does not exist, return 0. 135 if (!entry) { 136 return Some(0); 137 } 138 139 uint64_t interactionId = (*entry)->InteractionId(); 140 141 // Step 8.9. Remove pendingKeyDowns[code]. 142 mPendingKeyDowns.Remove(code); 143 144 // Step 8.10. Return interactionId. 145 return Some(interactionId); 146 } 147 148 // Step 9. If type is compositionstart: 149 if (eventType == eCompositionStart) { 150 // Step 9.1 For each entry in the values of pendingKeyDowns: 151 for (auto iter = mPendingKeyDowns.Iter(); !iter.Done(); iter.Next()) { 152 PerformanceEventTiming* entry = iter.Data(); 153 // Step 9.1.1. Append entry to window’s entries to be queued. 154 entry->SetInteractionId(0); 155 } 156 157 // Step 9.2. Clear pendingKeyDowns. 158 mPendingKeyDowns.Clear(); 159 // Step 9.3. Return 0 160 return Some(0); 161 } 162 163 // Step 10. If type is input: 164 if (eventType == eEditorInput) { 165 // Step 10.1. If event is not an instance of InputEvent, return 0. 166 const auto* inputEvent = aEvent->AsEditorInputEvent(); 167 if (!inputEvent) { 168 return Some(0); 169 } 170 171 // Step 10.2. If event’s isComposing attribute value is false, return 0. 172 if (!inputEvent->mIsComposing) { 173 return Some(0); 174 } 175 176 mLastKeydownInteractionValue = Nothing(); 177 return Some(IncreaseInteractionValueAndCount()); 178 } 179 180 // Step 11. Otherwise (type is pointercancel, pointerup, or click): 181 182 MOZ_ASSERT(eventType == ePointerCancel || eventType == ePointerUp || 183 eventType == ePointerClick || eventType == eContextMenu, 184 "Unexpected event type"); 185 const auto* mouseEvent = aEvent->AsMouseEvent(); 186 // Step 11.1. Let pointerId be event’s pointerId attribute value. 187 auto pointerId = mouseEvent->pointerId; 188 189 // Step 11.2. If type is click: 190 if (eventType == ePointerClick) { 191 if (pointerId == kNonPointerId) { 192 // -1 pointerId is a reserved value to indicate events that were generated 193 // by something other than a pointer device, like keydown. 194 // Return the interaction value of the keydown event instead. 195 return Some(mLastKeydownInteractionValue.valueOr(0)); 196 } 197 198 // Step 11.2.2. Let value be pointerMap[pointerId]. 199 auto value = mPointerInteractionValueMap.MaybeGet(pointerId); 200 // Step 11.2.1. If pointerMap[pointerId] does not exist, return 0. 201 if (!value) { 202 return Some(0); 203 } 204 205 // Step 11.2.3. Remove pointerMap[pointerId]. 206 mPointerInteractionValueMap.Remove(pointerId); 207 // Step 11.2.4. Return value. 208 return Some(*value); 209 } 210 211 // Step 11.3. Assert that type is pointerup or pointercancel. 212 MOZ_RELEASE_ASSERT(eventType == ePointerUp || eventType == ePointerCancel || 213 eventType == eContextMenu); 214 215 // Step 11.5. Let pointerDownEntry be pendingPointerDowns[pointerId]. 216 auto entry = mPendingPointerDowns.MaybeGet(pointerId); 217 // Step 11.4. If pendingPointerDowns[pointerId] does not exist, return 0. 218 if (!entry) { 219 // This is the case where we have seen a pointerup before a contextmenu 220 // event. We return the same interactionId for the contextmenu. 221 // See https://github.com/w3c/event-timing/issues/155. 222 if (eventType == eContextMenu) { 223 return Some(mCurrentInteractionValue); 224 } 225 226 // This is the case where we have seen a contextmenu before a pointerup 227 // event. Similarly, we return the same interactionId, but also we reset the 228 // "is contextmenu triggered" flag to make sure that the next events are 229 // handled correctly. See https://github.com/w3c/event-timing/issues/155. 230 if (eventType == ePointerUp && mContextMenuTriggered) { 231 mContextMenuTriggered = false; 232 return Some(mCurrentInteractionValue); 233 } 234 return Some(0); 235 } 236 237 // Step 11.7. If type is pointerup: 238 if (eventType == ePointerUp || eventType == eContextMenu) { 239 // Step 11.7.1. Increase interaction count on window. 240 uint64_t interactionId = IncreaseInteractionValueAndCount(); 241 242 // Step 11.7.2. Set pointerMap[pointerId] to window’s user interaction 243 // value. 244 mPointerInteractionValueMap.InsertOrUpdate(pointerId, interactionId); 245 246 // Step 11.7.3. Set pointerDownEntry’s interactionId to 247 // pointerMap[pointerId]. 248 (*entry)->SetInteractionId(interactionId); 249 } else { 250 (*entry)->SetInteractionId(0); 251 } 252 253 // Step 11.9. Remove pendingPointerDowns[pointerId]. 254 mPendingPointerDowns.Remove(pointerId); 255 256 if (eventType == eContextMenu) { 257 mContextMenuTriggered = true; 258 } 259 260 // Step 11.10. If type is pointercancel, return 0. 261 if (eventType == ePointerCancel) { 262 return Some(0); 263 } 264 265 return Some(mPointerInteractionValueMap.Get(pointerId)); 266 } 267 268 } // namespace mozilla::dom