PerformanceObserver.cpp (12066B)
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 "PerformanceObserver.h" 8 9 #include "LargestContentfulPaint.h" 10 #include "PerformanceEntry.h" 11 #include "PerformanceObserverEntryList.h" 12 #include "mozilla/StaticPrefs_dom.h" 13 #include "mozilla/dom/Performance.h" 14 #include "mozilla/dom/PerformanceBinding.h" 15 #include "mozilla/dom/PerformanceEntryBinding.h" 16 #include "mozilla/dom/PerformanceObserverBinding.h" 17 #include "mozilla/dom/WorkerScope.h" 18 #include "nsIScriptError.h" 19 #include "nsPIDOMWindow.h" 20 #include "nsQueryObject.h" 21 #include "nsString.h" 22 23 using namespace mozilla; 24 using namespace mozilla::dom; 25 26 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PerformanceObserver) 27 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PerformanceObserver) 28 tmp->Disconnect(); 29 NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback) 30 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPerformance) 31 NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner) 32 NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries) 33 NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER 34 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 35 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PerformanceObserver) 36 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback) 37 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPerformance) 38 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner) 39 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries) 40 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 41 42 NS_IMPL_CYCLE_COLLECTING_ADDREF(PerformanceObserver) 43 NS_IMPL_CYCLE_COLLECTING_RELEASE(PerformanceObserver) 44 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceObserver) 45 NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY 46 NS_INTERFACE_MAP_ENTRY(nsISupports) 47 NS_INTERFACE_MAP_END 48 49 PerformanceObserver::PerformanceObserver(nsPIDOMWindowInner* aOwner, 50 PerformanceObserverCallback& aCb) 51 : mOwner(aOwner->AsGlobal()), 52 mCallback(&aCb), 53 mObserverType(ObserverTypeUndefined), 54 mConnected(false) { 55 MOZ_ASSERT(mOwner); 56 mPerformance = aOwner->GetPerformance(); 57 } 58 59 PerformanceObserver::PerformanceObserver(WorkerPrivate* aWorkerPrivate, 60 PerformanceObserverCallback& aCb) 61 : mOwner(aWorkerPrivate->GlobalScope()), 62 mCallback(&aCb), 63 mObserverType(ObserverTypeUndefined), 64 mConnected(false) { 65 MOZ_ASSERT(aWorkerPrivate->GlobalScope()); 66 mPerformance = aWorkerPrivate->GlobalScope()->GetPerformance(); 67 } 68 69 PerformanceObserver::~PerformanceObserver() { 70 Disconnect(); 71 MOZ_ASSERT(!mConnected); 72 } 73 74 // static 75 already_AddRefed<PerformanceObserver> PerformanceObserver::Constructor( 76 const GlobalObject& aGlobal, PerformanceObserverCallback& aCb, 77 ErrorResult& aRv) { 78 if (NS_IsMainThread()) { 79 nsCOMPtr<nsPIDOMWindowInner> ownerWindow = 80 do_QueryInterface(aGlobal.GetAsSupports()); 81 if (!ownerWindow) { 82 aRv.Throw(NS_ERROR_FAILURE); 83 return nullptr; 84 } 85 86 RefPtr<PerformanceObserver> observer = 87 new PerformanceObserver(ownerWindow, aCb); 88 return observer.forget(); 89 } 90 91 JSContext* cx = aGlobal.Context(); 92 WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); 93 MOZ_ASSERT(workerPrivate); 94 95 RefPtr<PerformanceObserver> observer = 96 new PerformanceObserver(workerPrivate, aCb); 97 return observer.forget(); 98 } 99 100 JSObject* PerformanceObserver::WrapObject(JSContext* aCx, 101 JS::Handle<JSObject*> aGivenProto) { 102 return PerformanceObserver_Binding::Wrap(aCx, this, aGivenProto); 103 } 104 105 void PerformanceObserver::Notify() { 106 if (mQueuedEntries.IsEmpty()) { 107 return; 108 } 109 RefPtr<PerformanceObserverEntryList> list = 110 new PerformanceObserverEntryList(this, mQueuedEntries); 111 112 mQueuedEntries.Clear(); 113 114 ErrorResult rv; 115 RefPtr<PerformanceObserverCallback> callback(mCallback); 116 callback->Call(this, *list, *this, rv); 117 if (NS_WARN_IF(rv.Failed())) { 118 rv.SuppressException(); 119 } 120 } 121 122 void PerformanceObserver::QueueEntry(PerformanceEntry* aEntry) { 123 MOZ_ASSERT(aEntry); 124 MOZ_ASSERT(ObservesTypeOfEntry(aEntry)); 125 126 mQueuedEntries.AppendElement(aEntry); 127 } 128 129 static constexpr nsLiteralString kValidEventTimingNames[2] = { 130 u"event"_ns, u"first-input"_ns}; 131 132 /* 133 * Keep this list in alphabetical order. 134 * https://w3c.github.io/performance-timeline/#supportedentrytypes-attribute 135 */ 136 static constexpr nsLiteralString kValidTypeNames[5] = { 137 u"mark"_ns, u"measure"_ns, u"navigation"_ns, u"paint"_ns, u"resource"_ns, 138 }; 139 140 void PerformanceObserver::Observe(const PerformanceObserverInit& aOptions, 141 ErrorResult& aRv) { 142 const Optional<Sequence<nsString>>& maybeEntryTypes = aOptions.mEntryTypes; 143 const Optional<nsString>& maybeType = aOptions.mType; 144 const Optional<bool>& maybeBuffered = aOptions.mBuffered; 145 146 if (!mPerformance || !mOwner) { 147 aRv.Throw(NS_ERROR_FAILURE); 148 return; 149 } 150 151 if (!maybeEntryTypes.WasPassed() && !maybeType.WasPassed()) { 152 /* Per spec (3.3.1.2), this should be a syntax error. */ 153 aRv.ThrowTypeError("Can't call observe without `type` or `entryTypes`"); 154 return; 155 } 156 157 if (maybeEntryTypes.WasPassed() && maybeType.WasPassed()) { 158 /* Per spec (3.3.1.3), this, too, should be a syntax error. */ 159 /* 160 * As per the spec we also need to throw a type error if there are both 161 * `entryTypes` and `buffered` options, but either Blink or WebKit doesn't 162 * throw the error so we don't throw to align the behavior with them. 163 * https://github.com/w3c/performance-timeline/issues/215 164 */ 165 aRv.ThrowTypeError("Can't call observe with both `type` and `entryTypes`"); 166 return; 167 } 168 169 /* 3.3.1.4.1 */ 170 if (mObserverType == ObserverTypeUndefined) { 171 if (maybeEntryTypes.WasPassed()) { 172 mObserverType = ObserverTypeMultiple; 173 } else { 174 mObserverType = ObserverTypeSingle; 175 } 176 } 177 178 /* 3.3.1.4.2 */ 179 if (mObserverType == ObserverTypeSingle && maybeEntryTypes.WasPassed()) { 180 aRv.Throw(NS_ERROR_DOM_INVALID_MODIFICATION_ERR); 181 return; 182 } 183 /* 3.3.1.4.3 */ 184 if (mObserverType == ObserverTypeMultiple && maybeType.WasPassed()) { 185 aRv.Throw(NS_ERROR_DOM_INVALID_MODIFICATION_ERR); 186 return; 187 } 188 189 bool needQueueNotificationObserverTask = false; 190 /* 3.3.1.5 */ 191 if (mObserverType == ObserverTypeMultiple) { 192 const Sequence<nsString>& entryTypes = maybeEntryTypes.Value(); 193 194 if (entryTypes.IsEmpty()) { 195 return; 196 } 197 198 /* 3.3.1.5.2 */ 199 nsTArray<nsString> validEntryTypes; 200 201 if (StaticPrefs::dom_enable_event_timing()) { 202 for (const nsLiteralString& name : kValidEventTimingNames) { 203 if (entryTypes.Contains(name) && !validEntryTypes.Contains(name)) { 204 validEntryTypes.AppendElement(name); 205 } 206 } 207 } 208 if (StaticPrefs::dom_enable_largest_contentful_paint()) { 209 if (entryTypes.Contains(kLargestContentfulPaintName) && 210 !validEntryTypes.Contains(kLargestContentfulPaintName)) { 211 validEntryTypes.AppendElement(kLargestContentfulPaintName); 212 } 213 } 214 for (const nsLiteralString& name : kValidTypeNames) { 215 if (entryTypes.Contains(name) && !validEntryTypes.Contains(name)) { 216 validEntryTypes.AppendElement(name); 217 } 218 } 219 220 nsAutoString invalidTypesJoined; 221 bool addComma = false; 222 for (const auto& type : entryTypes) { 223 if (!validEntryTypes.Contains<nsString>(type)) { 224 if (addComma) { 225 invalidTypesJoined.AppendLiteral(", "); 226 } 227 addComma = true; 228 invalidTypesJoined.Append(type); 229 } 230 } 231 232 if (!invalidTypesJoined.IsEmpty()) { 233 AutoTArray<nsString, 1> params = {invalidTypesJoined}; 234 mOwner->ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, 235 nsContentUtils::eDOM_PROPERTIES, 236 "UnsupportedEntryTypesIgnored"_ns, params); 237 // (we don't return because we're ignoring and we keep going) 238 } 239 240 /* 3.3.1.5.3 */ 241 if (validEntryTypes.IsEmpty()) { 242 mOwner->ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, 243 nsContentUtils::eDOM_PROPERTIES, 244 "AllEntryTypesIgnored"_ns); 245 return; 246 } 247 248 /* 249 * Registered or not, we clear out the list of options, and start fresh 250 * with the one that we are using here. (3.3.1.5.4,5) 251 */ 252 mOptions.Clear(); 253 mOptions.AppendElement(aOptions); 254 255 } else { 256 MOZ_ASSERT(mObserverType == ObserverTypeSingle); 257 bool typeValid = false; 258 nsString type = maybeType.Value(); 259 260 /* 3.3.1.6.2 */ 261 if (StaticPrefs::dom_enable_event_timing()) { 262 for (const nsLiteralString& name : kValidEventTimingNames) { 263 if (type == name) { 264 typeValid = true; 265 break; 266 } 267 } 268 } 269 for (const nsLiteralString& name : kValidTypeNames) { 270 if (type == name) { 271 typeValid = true; 272 break; 273 } 274 } 275 276 if (StaticPrefs::dom_enable_largest_contentful_paint()) { 277 if (type == kLargestContentfulPaintName) { 278 typeValid = true; 279 } 280 } 281 282 if (!typeValid) { 283 AutoTArray<nsString, 1> params = {type}; 284 mOwner->ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, 285 nsContentUtils::eDOM_PROPERTIES, 286 "UnsupportedEntryTypesIgnored"_ns, params); 287 return; 288 } 289 290 /* 3.3.1.6.4, 3.3.1.6.4 */ 291 bool didUpdateOptionsList = false; 292 nsTArray<PerformanceObserverInit> updatedOptionsList; 293 for (auto& option : mOptions) { 294 if (option.mType.WasPassed() && option.mType.Value() == type) { 295 updatedOptionsList.AppendElement(aOptions); 296 didUpdateOptionsList = true; 297 } else { 298 updatedOptionsList.AppendElement(option); 299 } 300 } 301 if (!didUpdateOptionsList) { 302 updatedOptionsList.AppendElement(aOptions); 303 } 304 mOptions = std::move(updatedOptionsList); 305 306 /* 3.3.1.6.5 */ 307 if (maybeBuffered.WasPassed() && maybeBuffered.Value()) { 308 nsTArray<RefPtr<PerformanceEntry>> existingEntries; 309 mPerformance->GetEntriesByTypeForObserver(type, existingEntries); 310 if (!existingEntries.IsEmpty()) { 311 mQueuedEntries.AppendElements(existingEntries); 312 needQueueNotificationObserverTask = true; 313 } 314 } 315 } 316 /* Add ourselves to the list of registered performance 317 * observers, if necessary. (3.3.1.5.4,5; 3.3.1.6.4) 318 */ 319 mPerformance->AddObserver(this); 320 321 if (needQueueNotificationObserverTask) { 322 mPerformance->QueueNotificationObserversTask(); 323 } 324 mConnected = true; 325 } 326 327 void PerformanceObserver::GetSupportedEntryTypes( 328 const GlobalObject& aGlobal, JS::MutableHandle<JSObject*> aObject) { 329 nsTArray<nsString> validTypes; 330 JS::Rooted<JS::Value> val(aGlobal.Context()); 331 332 if (StaticPrefs::dom_enable_event_timing()) { 333 for (const nsLiteralString& name : kValidEventTimingNames) { 334 validTypes.AppendElement(name); 335 } 336 } 337 338 if (StaticPrefs::dom_enable_largest_contentful_paint()) { 339 validTypes.AppendElement(u"largest-contentful-paint"_ns); 340 } 341 for (const nsLiteralString& name : kValidTypeNames) { 342 validTypes.AppendElement(name); 343 } 344 345 if (!ToJSValue(aGlobal.Context(), validTypes, &val)) { 346 /* 347 * If this conversion fails, we don't set a result. 348 * The spec does not allow us to throw an exception. 349 */ 350 return; 351 } 352 aObject.set(&val.toObject()); 353 } 354 355 bool PerformanceObserver::ObservesTypeOfEntry(PerformanceEntry* aEntry) { 356 for (auto& option : mOptions) { 357 if (aEntry->ShouldAddEntryToObserverBuffer(option)) { 358 return true; 359 } 360 } 361 return false; 362 } 363 364 void PerformanceObserver::Disconnect() { 365 if (mConnected) { 366 MOZ_ASSERT(mPerformance); 367 mPerformance->RemoveObserver(this); 368 mOptions.Clear(); 369 mConnected = false; 370 } 371 } 372 373 void PerformanceObserver::TakeRecords( 374 nsTArray<RefPtr<PerformanceEntry>>& aRetval) { 375 MOZ_ASSERT(aRetval.IsEmpty()); 376 aRetval = std::move(mQueuedEntries); 377 }