Performance.cpp (40801B)
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 "Performance.h" 8 9 #include <sstream> 10 11 #if defined(XP_LINUX) 12 # include <fcntl.h> 13 # include <sys/mman.h> 14 #endif 15 16 #include "ETWTools.h" 17 #include "GeckoProfiler.h" 18 #include "PerformanceEntry.h" 19 #include "PerformanceMainThread.h" 20 #include "PerformanceMark.h" 21 #include "PerformanceMeasure.h" 22 #include "PerformanceObserver.h" 23 #include "PerformanceResourceTiming.h" 24 #include "PerformanceService.h" 25 #include "PerformanceWorker.h" 26 #include "mozilla/BasePrincipal.h" 27 #include "mozilla/ErrorResult.h" 28 #include "mozilla/IntegerPrintfMacros.h" 29 #include "mozilla/Perfetto.h" 30 #include "mozilla/Preferences.h" 31 #include "mozilla/TimeStamp.h" 32 #include "mozilla/dom/MessagePortBinding.h" 33 #include "mozilla/dom/PerformanceBinding.h" 34 #include "mozilla/dom/PerformanceEntryEvent.h" 35 #include "mozilla/dom/PerformanceNavigationBinding.h" 36 #include "mozilla/dom/PerformanceNavigationTiming.h" 37 #include "mozilla/dom/PerformanceObserverBinding.h" 38 #include "mozilla/dom/WorkerPrivate.h" 39 #include "mozilla/dom/WorkerRunnable.h" 40 #include "mozilla/dom/WorkerScope.h" 41 #include "nsGlobalWindowInner.h" 42 #include "nsRFPService.h" 43 44 #define PERFLOG(msg, ...) printf_stderr(msg, ##__VA_ARGS__) 45 46 namespace mozilla::dom { 47 48 enum class Performance::ResolveTimestampAttribute { 49 Start, 50 End, 51 Duration, 52 }; 53 54 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Performance) 55 NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) 56 57 NS_IMPL_CYCLE_COLLECTION_INHERITED(Performance, DOMEventTargetHelper, 58 mUserEntries, mResourceEntries, 59 mSecondaryResourceEntries, mObservers); 60 61 NS_IMPL_ADDREF_INHERITED(Performance, DOMEventTargetHelper) 62 NS_IMPL_RELEASE_INHERITED(Performance, DOMEventTargetHelper) 63 64 // Used to dump performance timing information to a local file. 65 // Only defined when the env variable MOZ_USE_PERFORMANCE_MARKER_FILE 66 // is set and initialized by MaybeOpenMarkerFile(). 67 static FILE* sMarkerFile = nullptr; 68 69 /* static */ 70 already_AddRefed<Performance> Performance::CreateForMainThread( 71 nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, 72 nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel) { 73 MOZ_ASSERT(NS_IsMainThread()); 74 75 MOZ_ASSERT(aWindow->AsGlobal()); 76 RefPtr<Performance> performance = 77 new PerformanceMainThread(aWindow, aDOMTiming, aChannel); 78 return performance.forget(); 79 } 80 81 /* static */ 82 already_AddRefed<Performance> Performance::CreateForWorker( 83 WorkerGlobalScope* aGlobalScope) { 84 MOZ_ASSERT(aGlobalScope); 85 // aWorkerPrivate->AssertIsOnWorkerThread(); 86 87 RefPtr<Performance> performance = new PerformanceWorker(aGlobalScope); 88 return performance.forget(); 89 } 90 91 /* static */ 92 already_AddRefed<Performance> Performance::Get(JSContext* aCx, 93 nsIGlobalObject* aGlobal) { 94 RefPtr<Performance> performance; 95 if (NS_IsMainThread()) { 96 nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal); 97 if (!window) { 98 return nullptr; 99 } 100 101 performance = window->GetPerformance(); 102 return performance.forget(); 103 } 104 105 const WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx); 106 if (!workerPrivate) { 107 return nullptr; 108 } 109 110 WorkerGlobalScope* scope = workerPrivate->GlobalScope(); 111 MOZ_ASSERT(scope); 112 performance = scope->GetPerformance(); 113 114 return performance.forget(); 115 } 116 117 Performance::Performance(nsIGlobalObject* aGlobal) 118 : DOMEventTargetHelper(aGlobal), 119 mResourceTimingBufferSize(kDefaultResourceTimingBufferSize), 120 mPendingNotificationObserversTask(false), 121 mPendingResourceTimingBufferFullEvent(false), 122 mRTPCallerType(aGlobal->GetRTPCallerType()), 123 mCrossOriginIsolated(aGlobal->CrossOriginIsolated()), 124 mShouldResistFingerprinting(aGlobal->ShouldResistFingerprinting( 125 RFPTarget::ReduceTimerPrecision)) {} 126 127 Performance::~Performance() = default; 128 129 DOMHighResTimeStamp Performance::TimeStampToDOMHighResForRendering( 130 TimeStamp aTimeStamp) const { 131 DOMHighResTimeStamp stamp = GetDOMTiming()->TimeStampToDOMHighRes(aTimeStamp); 132 // 0 is an inappropriate mixin for this this area; however CSS Animations 133 // needs to have it's Time Reduction Logic refactored, so it's currently 134 // only clamping for RFP mode. RFP mode gives a much lower time precision, 135 // so we accept the security leak here for now. 136 return nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly(stamp, 0, 137 mRTPCallerType); 138 } 139 140 DOMHighResTimeStamp Performance::Now() { 141 DOMHighResTimeStamp rawTime = NowUnclamped(); 142 143 // XXX: Removing this caused functions in pkcs11f.h to fail. 144 // Bug 1628021 investigates the root cause - it involves initializing 145 // the RNG service (part of GetRandomTimelineSeed()) off-main-thread 146 // but the underlying cause hasn't been identified yet. 147 if (mRTPCallerType == RTPCallerType::SystemPrincipal) { 148 return rawTime; 149 } 150 151 return nsRFPService::ReduceTimePrecisionAsMSecs( 152 rawTime, GetRandomTimelineSeed(), mRTPCallerType); 153 } 154 155 DOMHighResTimeStamp Performance::NowUnclamped() const { 156 TimeDuration duration = TimeStamp::Now() - CreationTimeStamp(); 157 return duration.ToMilliseconds(); 158 } 159 160 DOMHighResTimeStamp Performance::TimeOrigin() { 161 if (!mPerformanceService) { 162 mPerformanceService = PerformanceService::GetOrCreate(); 163 } 164 165 MOZ_ASSERT(mPerformanceService); 166 DOMHighResTimeStamp rawTimeOrigin = 167 mPerformanceService->TimeOrigin(CreationTimeStamp()); 168 // Time Origin is an absolute timestamp, so we supply a 0 context mix-in 169 return nsRFPService::ReduceTimePrecisionAsMSecs(rawTimeOrigin, 0, 170 mRTPCallerType); 171 } 172 173 JSObject* Performance::WrapObject(JSContext* aCx, 174 JS::Handle<JSObject*> aGivenProto) { 175 return Performance_Binding::Wrap(aCx, this, aGivenProto); 176 } 177 178 void Performance::GetEntries(nsTArray<RefPtr<PerformanceEntry>>& aRetval) { 179 aRetval = mResourceEntries.Clone(); 180 aRetval.AppendElements(mUserEntries); 181 aRetval.Sort(PerformanceEntryComparator()); 182 } 183 184 void Performance::GetEntriesByType( 185 const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) { 186 RefPtr<nsAtom> entryType = NS_Atomize(aEntryType); 187 if (entryType == nsGkAtoms::resource) { 188 aRetval = mResourceEntries.Clone(); 189 return; 190 } 191 192 aRetval.Clear(); 193 194 if (entryType == nsGkAtoms::mark || entryType == nsGkAtoms::measure) { 195 for (PerformanceEntry* entry : mUserEntries) { 196 if (entry->GetEntryType() == entryType) { 197 aRetval.AppendElement(entry); 198 } 199 } 200 } 201 } 202 203 void Performance::GetEntriesByName( 204 const nsAString& aName, const Optional<nsAString>& aEntryType, 205 nsTArray<RefPtr<PerformanceEntry>>& aRetval) { 206 aRetval.Clear(); 207 208 RefPtr<nsAtom> name = NS_Atomize(aName); 209 RefPtr<nsAtom> entryType = 210 aEntryType.WasPassed() ? NS_Atomize(aEntryType.Value()) : nullptr; 211 212 if (entryType) { 213 if (entryType == nsGkAtoms::mark || entryType == nsGkAtoms::measure) { 214 for (PerformanceEntry* entry : mUserEntries) { 215 if (entry->GetName() == name && entry->GetEntryType() == entryType) { 216 aRetval.AppendElement(entry); 217 } 218 } 219 return; 220 } 221 if (entryType == nsGkAtoms::resource) { 222 for (PerformanceEntry* entry : mResourceEntries) { 223 MOZ_ASSERT(entry->GetEntryType() == entryType); 224 if (entry->GetName() == name) { 225 aRetval.AppendElement(entry); 226 } 227 } 228 return; 229 } 230 // Invalid entryType 231 return; 232 } 233 234 nsTArray<PerformanceEntry*> qualifiedResourceEntries; 235 nsTArray<PerformanceEntry*> qualifiedUserEntries; 236 // ::Measure expects that results from this function are already 237 // passed through ReduceTimePrecision. mResourceEntries and mUserEntries 238 // are, so the invariant holds. 239 for (PerformanceEntry* entry : mResourceEntries) { 240 if (entry->GetName() == name) { 241 qualifiedResourceEntries.AppendElement(entry); 242 } 243 } 244 245 for (PerformanceEntry* entry : mUserEntries) { 246 if (entry->GetName() == name) { 247 qualifiedUserEntries.AppendElement(entry); 248 } 249 } 250 251 size_t resourceEntriesIdx = 0, userEntriesIdx = 0; 252 aRetval.SetCapacity(qualifiedResourceEntries.Length() + 253 qualifiedUserEntries.Length()); 254 255 PerformanceEntryComparator comparator; 256 257 while (resourceEntriesIdx < qualifiedResourceEntries.Length() && 258 userEntriesIdx < qualifiedUserEntries.Length()) { 259 if (comparator.LessThan(qualifiedResourceEntries[resourceEntriesIdx], 260 qualifiedUserEntries[userEntriesIdx])) { 261 aRetval.AppendElement(qualifiedResourceEntries[resourceEntriesIdx]); 262 ++resourceEntriesIdx; 263 } else { 264 aRetval.AppendElement(qualifiedUserEntries[userEntriesIdx]); 265 ++userEntriesIdx; 266 } 267 } 268 269 while (resourceEntriesIdx < qualifiedResourceEntries.Length()) { 270 aRetval.AppendElement(qualifiedResourceEntries[resourceEntriesIdx]); 271 ++resourceEntriesIdx; 272 } 273 274 while (userEntriesIdx < qualifiedUserEntries.Length()) { 275 aRetval.AppendElement(qualifiedUserEntries[userEntriesIdx]); 276 ++userEntriesIdx; 277 } 278 } 279 280 void Performance::GetEntriesByTypeForObserver( 281 const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) { 282 GetEntriesByType(aEntryType, aRetval); 283 } 284 285 void Performance::ClearUserEntries(const Optional<nsAString>& aEntryName, 286 const nsAString& aEntryType) { 287 MOZ_ASSERT(!aEntryType.IsEmpty()); 288 RefPtr<nsAtom> name = 289 aEntryName.WasPassed() ? NS_Atomize(aEntryName.Value()) : nullptr; 290 RefPtr<nsAtom> entryType = NS_Atomize(aEntryType); 291 mUserEntries.RemoveElementsBy([name, entryType](const auto& entry) { 292 return (!name || entry->GetName() == name) && 293 (entry->GetEntryType() == entryType); 294 }); 295 } 296 297 void Performance::ClearResourceTimings() { mResourceEntries.Clear(); } 298 299 struct UserTimingMarker : public BaseMarkerType<UserTimingMarker> { 300 static constexpr const char* Name = "UserTiming"; 301 static constexpr const char* Description = 302 "UserTimingMeasure is created using the DOM API performance.measure()."; 303 304 using MS = MarkerSchema; 305 static constexpr MS::PayloadField PayloadFields[] = { 306 {"name", MS::InputType::String, "User Marker Name", MS::Format::String, 307 MS::PayloadFlags::Searchable}, 308 {"entryType", MS::InputType::Boolean, "Entry Type"}, 309 {"startMark", MS::InputType::String, "Start Mark"}, 310 {"endMark", MS::InputType::String, "End Mark"}}; 311 312 static constexpr MS::Location Locations[] = {MS::Location::MarkerChart, 313 MS::Location::MarkerTable}; 314 static constexpr const char* AllLabels = "{marker.data.name}"; 315 316 static constexpr MS::ETWMarkerGroup Group = MS::ETWMarkerGroup::UserMarkers; 317 318 static void StreamJSONMarkerData( 319 baseprofiler::SpliceableJSONWriter& aWriter, 320 const ProfilerString16View& aName, bool aIsMeasure, 321 const Maybe<ProfilerString16View>& aStartMark, 322 const Maybe<ProfilerString16View>& aEndMark) { 323 StreamJSONMarkerDataImpl( 324 aWriter, aName, 325 aIsMeasure ? MakeStringSpan("measure") : MakeStringSpan("mark"), 326 aStartMark, aEndMark); 327 } 328 }; 329 330 already_AddRefed<PerformanceMark> Performance::Mark( 331 JSContext* aCx, const nsAString& aName, 332 const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) { 333 nsCOMPtr<nsIGlobalObject> parent = GetParentObject(); 334 if (!parent || parent->IsDying() || !parent->HasJSGlobal()) { 335 aRv.ThrowInvalidStateError("Global object is unavailable"); 336 return nullptr; 337 } 338 339 GlobalObject global(aCx, parent->GetGlobalJSObject()); 340 if (global.Failed()) { 341 aRv.ThrowInvalidStateError("Global object is unavailable"); 342 return nullptr; 343 } 344 345 RefPtr<PerformanceMark> performanceMark = 346 PerformanceMark::Constructor(global, aName, aMarkOptions, aRv); 347 if (aRv.Failed()) { 348 return nullptr; 349 } 350 351 InsertUserEntry(performanceMark); 352 353 if (profiler_thread_is_being_profiled_for_markers()) { 354 Maybe<uint64_t> innerWindowId; 355 if (nsGlobalWindowInner* owner = GetOwnerWindow()) { 356 innerWindowId = Some(owner->WindowID()); 357 } 358 TimeStamp startTimeStamp = 359 CreationTimeStamp() + 360 TimeDuration::FromMilliseconds(performanceMark->UnclampedStartTime()); 361 profiler_add_marker("UserTiming", geckoprofiler::category::DOM, 362 MarkerOptions(MarkerTiming::InstantAt(startTimeStamp), 363 MarkerInnerWindowId(innerWindowId)), 364 UserTimingMarker{}, aName, /* aIsMeasure */ false, 365 Nothing{}, Nothing{}); 366 } 367 368 return performanceMark.forget(); 369 } 370 371 void Performance::ClearMarks(const Optional<nsAString>& aName) { 372 ClearUserEntries(aName, u"mark"_ns); 373 } 374 375 // To be removed once bug 1124165 lands 376 bool Performance::IsPerformanceTimingAttribute(const nsAString& aName) const { 377 // Note that toJSON is added to this list due to bug 1047848 378 static const char* attributes[] = {"navigationStart", 379 "unloadEventStart", 380 "unloadEventEnd", 381 "redirectStart", 382 "redirectEnd", 383 "fetchStart", 384 "domainLookupStart", 385 "domainLookupEnd", 386 "connectStart", 387 "secureConnectionStart", 388 "connectEnd", 389 "requestStart", 390 "responseStart", 391 "responseEnd", 392 "domLoading", 393 "domInteractive", 394 "domContentLoadedEventStart", 395 "domContentLoadedEventEnd", 396 "domComplete", 397 "loadEventStart", 398 "loadEventEnd", 399 nullptr}; 400 401 for (uint32_t i = 0; attributes[i]; ++i) { 402 if (aName.EqualsASCII(attributes[i])) { 403 return true; 404 } 405 } 406 407 return false; 408 } 409 410 DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithString( 411 const nsAString& aName, ErrorResult& aRv, bool aReturnUnclamped) { 412 if (IsPerformanceTimingAttribute(aName)) { 413 return ConvertNameToTimestamp(aName, aRv); 414 } 415 416 RefPtr<nsAtom> name = NS_Atomize(aName); 417 // Just loop over the user entries 418 for (const PerformanceEntry* entry : Reversed(mUserEntries)) { 419 if (entry->GetName() == name && entry->GetEntryType() == nsGkAtoms::mark) { 420 if (aReturnUnclamped) { 421 return entry->UnclampedStartTime(); 422 } 423 return entry->StartTime(); 424 } 425 } 426 427 nsPrintfCString errorMsg("Given mark name, %s, is unknown", 428 NS_ConvertUTF16toUTF8(aName).get()); 429 aRv.ThrowSyntaxError(errorMsg); 430 return 0; 431 } 432 433 DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithDOMHighResTimeStamp( 434 const ResolveTimestampAttribute aAttribute, 435 const DOMHighResTimeStamp aTimestamp, ErrorResult& aRv) { 436 if (aTimestamp < 0) { 437 nsAutoCString attributeName; 438 switch (aAttribute) { 439 case ResolveTimestampAttribute::Start: 440 attributeName = "start"; 441 break; 442 case ResolveTimestampAttribute::End: 443 attributeName = "end"; 444 break; 445 case ResolveTimestampAttribute::Duration: 446 attributeName = "duration"; 447 break; 448 } 449 450 nsPrintfCString errorMsg("Given attribute %s cannot be negative", 451 attributeName.get()); 452 aRv.ThrowTypeError(errorMsg); 453 } 454 return aTimestamp; 455 } 456 457 DOMHighResTimeStamp Performance::ConvertMarkToTimestamp( 458 const ResolveTimestampAttribute aAttribute, 459 const OwningStringOrDouble& aMarkNameOrTimestamp, ErrorResult& aRv, 460 bool aReturnUnclamped) { 461 if (aMarkNameOrTimestamp.IsString()) { 462 return ConvertMarkToTimestampWithString(aMarkNameOrTimestamp.GetAsString(), 463 aRv, aReturnUnclamped); 464 } 465 466 return ConvertMarkToTimestampWithDOMHighResTimeStamp( 467 aAttribute, aMarkNameOrTimestamp.GetAsDouble(), aRv); 468 } 469 470 DOMHighResTimeStamp Performance::ConvertNameToTimestamp(const nsAString& aName, 471 ErrorResult& aRv) { 472 if (!IsGlobalObjectWindow()) { 473 nsPrintfCString errorMsg( 474 "Cannot get PerformanceTiming attribute values for non-Window global " 475 "object. Given: %s", 476 NS_ConvertUTF16toUTF8(aName).get()); 477 aRv.ThrowTypeError(errorMsg); 478 return 0; 479 } 480 481 if (aName.EqualsASCII("navigationStart")) { 482 return 0; 483 } 484 485 // We use GetPerformanceTimingFromString, rather than calling the 486 // navigationStart method timing function directly, because the former handles 487 // reducing precision against timing attacks. 488 const DOMHighResTimeStamp startTime = 489 GetPerformanceTimingFromString(u"navigationStart"_ns); 490 const DOMHighResTimeStamp endTime = GetPerformanceTimingFromString(aName); 491 MOZ_ASSERT(endTime >= 0); 492 if (endTime == 0) { 493 nsPrintfCString errorMsg( 494 "Given PerformanceTiming attribute, %s, isn't available yet", 495 NS_ConvertUTF16toUTF8(aName).get()); 496 aRv.ThrowInvalidAccessError(errorMsg); 497 return 0; 498 } 499 500 return endTime - startTime; 501 } 502 503 DOMHighResTimeStamp Performance::ResolveEndTimeForMeasure( 504 const Optional<nsAString>& aEndMark, 505 const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv, 506 bool aReturnUnclamped) { 507 DOMHighResTimeStamp endTime; 508 if (aEndMark.WasPassed()) { 509 endTime = ConvertMarkToTimestampWithString(aEndMark.Value(), aRv, 510 aReturnUnclamped); 511 } else if (aOptions && aOptions->mEnd.WasPassed()) { 512 endTime = 513 ConvertMarkToTimestamp(ResolveTimestampAttribute::End, 514 aOptions->mEnd.Value(), aRv, aReturnUnclamped); 515 } else if (aOptions && aOptions->mStart.WasPassed() && 516 aOptions->mDuration.WasPassed()) { 517 const DOMHighResTimeStamp start = 518 ConvertMarkToTimestamp(ResolveTimestampAttribute::Start, 519 aOptions->mStart.Value(), aRv, aReturnUnclamped); 520 if (aRv.Failed()) { 521 return 0; 522 } 523 524 const DOMHighResTimeStamp duration = 525 ConvertMarkToTimestampWithDOMHighResTimeStamp( 526 ResolveTimestampAttribute::Duration, aOptions->mDuration.Value(), 527 aRv); 528 if (aRv.Failed()) { 529 return 0; 530 } 531 532 endTime = start + duration; 533 } else if (aReturnUnclamped) { 534 MOZ_DIAGNOSTIC_ASSERT(sMarkerFile || 535 profiler_thread_is_being_profiled_for_markers()); 536 endTime = NowUnclamped(); 537 } else { 538 endTime = Now(); 539 } 540 541 return endTime; 542 } 543 544 DOMHighResTimeStamp Performance::ResolveStartTimeForMeasure( 545 const Maybe<const nsAString&>& aStartMark, 546 const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv, 547 bool aReturnUnclamped) { 548 DOMHighResTimeStamp startTime; 549 if (aOptions && aOptions->mStart.WasPassed()) { 550 startTime = 551 ConvertMarkToTimestamp(ResolveTimestampAttribute::Start, 552 aOptions->mStart.Value(), aRv, aReturnUnclamped); 553 } else if (aOptions && aOptions->mDuration.WasPassed() && 554 aOptions->mEnd.WasPassed()) { 555 const DOMHighResTimeStamp duration = 556 ConvertMarkToTimestampWithDOMHighResTimeStamp( 557 ResolveTimestampAttribute::Duration, aOptions->mDuration.Value(), 558 aRv); 559 if (aRv.Failed()) { 560 return 0; 561 } 562 563 const DOMHighResTimeStamp end = 564 ConvertMarkToTimestamp(ResolveTimestampAttribute::End, 565 aOptions->mEnd.Value(), aRv, aReturnUnclamped); 566 if (aRv.Failed()) { 567 return 0; 568 } 569 570 startTime = end - duration; 571 } else if (aStartMark) { 572 startTime = 573 ConvertMarkToTimestampWithString(*aStartMark, aRv, aReturnUnclamped); 574 } else { 575 startTime = 0; 576 } 577 578 return startTime; 579 } 580 581 static std::string GetMarkerFilename() { 582 std::stringstream s; 583 if (char* markerDir = getenv("MOZ_PERFORMANCE_MARKER_DIR")) { 584 s << markerDir << "/"; 585 } 586 #ifdef XP_WIN 587 s << "marker-" << GetCurrentProcessId() << ".txt"; 588 #else 589 s << "marker-" << getpid() << ".txt"; 590 #endif 591 return s.str(); 592 } 593 594 Maybe<std::pair<TimeStamp, TimeStamp>> Performance::GetTimeStampsForMarker( 595 const Maybe<const nsAString&>& aStartMark, 596 const Optional<nsAString>& aEndMark, 597 const Maybe<const PerformanceMeasureOptions&>& aOptions) { 598 ErrorResult err; 599 const DOMHighResTimeStamp unclampedStartTime = ResolveStartTimeForMeasure( 600 aStartMark, aOptions, err, /* aReturnUnclamped */ true); 601 const DOMHighResTimeStamp unclampedEndTime = 602 ResolveEndTimeForMeasure(aEndMark, aOptions, /* aReturnUnclamped */ 603 err, true); 604 605 if (err.Failed()) { 606 return Nothing(); 607 } 608 609 // Performance.measure() can receive user-supplied timestamps and those 610 // timestamps might not be relative to 'navigation start'. This is 611 // (potentially) valid but, if we treat them as relative, we will end up 612 // placing them far into the future which causes problems for the profiler 613 // later so we report that as an error. (See bug 1925191 for details.) 614 // kMaxFuture_ms represents approximately 10 years worth of milliseconds. 615 static constexpr double kMaxFuture_ms = 31536000000.0; 616 if (unclampedStartTime > kMaxFuture_ms || unclampedEndTime > kMaxFuture_ms) { 617 return Nothing(); 618 } 619 620 TimeStamp startTimeStamp = 621 CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedStartTime); 622 TimeStamp endTimeStamp = 623 CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedEndTime); 624 625 return Some(std::make_pair(startTimeStamp, endTimeStamp)); 626 } 627 628 // Try to open the marker file for writing performance markers. 629 // If successful, returns true and sMarkerFile will be defined 630 // to the file handle. Otherwise, return false and sMarkerFile 631 // is NULL. 632 static bool MaybeOpenMarkerFile() { 633 if (!getenv("MOZ_USE_PERFORMANCE_MARKER_FILE")) { 634 return false; 635 } 636 637 // Check if it's already open. 638 if (sMarkerFile) { 639 return true; 640 } 641 642 #ifdef XP_LINUX 643 // We treat marker files similar to Jitdump files (see PerfSpewer.cpp) and 644 // mmap them if needed. 645 int fd = open(GetMarkerFilename().c_str(), O_CREAT | O_TRUNC | O_RDWR, 0666); 646 sMarkerFile = fdopen(fd, "w+"); 647 if (!sMarkerFile) { 648 return false; 649 } 650 651 // On Linux and Android, we need to mmap the file so that the path makes it 652 // into the perf.data file or into samply. 653 // On non-Android, make the mapping executable, otherwise the MMAP event may 654 // not be recorded by perf (see perf_event_open mmap_data). 655 // But on Android, don't make the mapping executable, because doing so can 656 // make the mmap call fail on some Android devices. It's also not required on 657 // Android because simpleperf sets mmap_data = 1 for unrelated reasons (it 658 // wants to know about vdex files for Java JIT profiling, see 659 // SetRecordNotExecutableMaps). 660 int protection = PROT_READ; 661 # ifndef ANDROID 662 protection |= PROT_EXEC; 663 # endif 664 665 // Mmap just the first page - that's enough to ensure the path makes it into 666 // the recording. 667 long page_size = sysconf(_SC_PAGESIZE); 668 void* mmap_address = mmap(nullptr, page_size, protection, MAP_PRIVATE, fd, 0); 669 if (mmap_address == MAP_FAILED) { 670 fclose(sMarkerFile); 671 sMarkerFile = nullptr; 672 return false; 673 } 674 #else 675 // On macOS, we just need to `open` or `fopen` the marker file, and samply 676 // will know its path because it hooks those functions - no mmap needed. 677 // On Windows, there's no need to use MOZ_USE_PERFORMANCE_MARKER_FILE because 678 // we have ETW trace events for UserTiming measures. Still, we want this code 679 // to compile successfully on Windows, so we use fopen rather than 680 // open+fdopen. 681 sMarkerFile = fopen(GetMarkerFilename().c_str(), "w+"); 682 if (!sMarkerFile) { 683 return false; 684 } 685 #endif 686 return true; 687 } 688 689 // This emits markers to an external marker-[pid].txt file for use by an 690 // external profiler like samply or etw-gecko 691 void Performance::MaybeEmitExternalProfilerMarker( 692 const nsAString& aName, Maybe<const PerformanceMeasureOptions&> aOptions, 693 Maybe<const nsAString&> aStartMark, const Optional<nsAString>& aEndMark) { 694 if (!MaybeOpenMarkerFile()) { 695 return; 696 } 697 698 #if defined(XP_LINUX) || defined(XP_WIN) || defined(XP_MACOSX) 699 Maybe<std::pair<TimeStamp, TimeStamp>> tsPair = 700 GetTimeStampsForMarker(aStartMark, aEndMark, aOptions); 701 if (tsPair.isNothing()) { 702 return; 703 } 704 auto [startTimeStamp, endTimeStamp] = tsPair.value(); 705 #endif 706 707 #ifdef XP_LINUX 708 uint64_t rawStart = startTimeStamp.RawClockMonotonicNanosecondsSinceBoot(); 709 uint64_t rawEnd = endTimeStamp.RawClockMonotonicNanosecondsSinceBoot(); 710 #elif XP_WIN 711 uint64_t rawStart = startTimeStamp.RawQueryPerformanceCounterValue(); 712 uint64_t rawEnd = endTimeStamp.RawQueryPerformanceCounterValue(); 713 #elif XP_MACOSX 714 uint64_t rawStart = startTimeStamp.RawMachAbsoluteTimeNanoseconds(); 715 uint64_t rawEnd = endTimeStamp.RawMachAbsoluteTimeNanoseconds(); 716 #else 717 uint64_t rawStart = 0; 718 uint64_t rawEnd = 0; 719 MOZ_CRASH("no timestamp"); 720 #endif 721 // Write a line for this measure to the marker file. The marker file uses a 722 // text-based format where every line is one marker, and each line has the 723 // format: 724 // `<raw_start_timestamp> <raw_end_timestamp> <measure_name>` 725 // 726 // The timestamp value is OS specific. 727 fprintf(sMarkerFile, "%" PRIu64 " %" PRIu64 " %s\n", rawStart, rawEnd, 728 NS_ConvertUTF16toUTF8(aName).get()); 729 fflush(sMarkerFile); 730 } 731 732 void MOZ_ALWAYS_INLINE Performance::MaybeAddProfileMarker( 733 const nsAString& aName, 734 const Maybe<const PerformanceMeasureOptions&>& options, 735 const Maybe<const nsAString&>& aStartMark, 736 const Optional<nsAString>& aEndMark) { 737 if (profiler_thread_is_being_profiled_for_markers()) { 738 AddProfileMarker(aName, options, aStartMark, aEndMark); 739 } 740 } 741 742 void MOZ_NEVER_INLINE Performance::AddProfileMarker( 743 const nsAString& aName, 744 const Maybe<const PerformanceMeasureOptions&>& options, 745 const Maybe<const nsAString&>& aStartMark, 746 const Optional<nsAString>& aEndMark) { 747 Maybe<std::pair<TimeStamp, TimeStamp>> tsPair = 748 GetTimeStampsForMarker(aStartMark, aEndMark, options); 749 if (tsPair.isNothing()) { 750 return; 751 } 752 auto [startTimeStamp, endTimeStamp] = tsPair.value(); 753 754 Maybe<nsString> endMark; 755 if (aEndMark.WasPassed()) { 756 endMark.emplace(aEndMark.Value()); 757 } 758 759 Maybe<uint64_t> innerWindowId; 760 if (nsGlobalWindowInner* owner = GetOwnerWindow()) { 761 innerWindowId = Some(owner->WindowID()); 762 } 763 profiler_add_marker("UserTiming", geckoprofiler::category::DOM, 764 {MarkerTiming::Interval(startTimeStamp, endTimeStamp), 765 MarkerInnerWindowId(innerWindowId)}, 766 UserTimingMarker{}, aName, /* aIsMeasure */ true, 767 aStartMark, endMark); 768 } 769 770 already_AddRefed<PerformanceMeasure> Performance::Measure( 771 JSContext* aCx, const nsAString& aName, 772 const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions, 773 const Optional<nsAString>& aEndMark, ErrorResult& aRv) { 774 if (!GetParentObject()) { 775 aRv.ThrowInvalidStateError("Global object is unavailable"); 776 return nullptr; 777 } 778 779 // Maybe is more readable than using the union type directly. 780 Maybe<const PerformanceMeasureOptions&> options; 781 if (aStartOrMeasureOptions.IsPerformanceMeasureOptions()) { 782 options.emplace(aStartOrMeasureOptions.GetAsPerformanceMeasureOptions()); 783 } 784 785 const bool isOptionsNotEmpty = 786 options.isSome() && 787 (!options->mDetail.isUndefined() || options->mStart.WasPassed() || 788 options->mEnd.WasPassed() || options->mDuration.WasPassed()); 789 if (isOptionsNotEmpty) { 790 if (aEndMark.WasPassed()) { 791 aRv.ThrowTypeError( 792 "Cannot provide separate endMark argument if " 793 "PerformanceMeasureOptions argument is given"); 794 return nullptr; 795 } 796 797 if (!options->mStart.WasPassed() && !options->mEnd.WasPassed()) { 798 aRv.ThrowTypeError( 799 "PerformanceMeasureOptions must have start and/or end member"); 800 return nullptr; 801 } 802 803 if (options->mStart.WasPassed() && options->mDuration.WasPassed() && 804 options->mEnd.WasPassed()) { 805 aRv.ThrowTypeError( 806 "PerformanceMeasureOptions cannot have all of the following members: " 807 "start, duration, and end"); 808 return nullptr; 809 } 810 } 811 812 const DOMHighResTimeStamp endTime = ResolveEndTimeForMeasure( 813 aEndMark, options, aRv, /* aReturnUnclamped */ false); 814 if (NS_WARN_IF(aRv.Failed())) { 815 return nullptr; 816 } 817 818 // Convert to Maybe for consistency with options. 819 Maybe<const nsAString&> startMark; 820 if (aStartOrMeasureOptions.IsString()) { 821 startMark.emplace(aStartOrMeasureOptions.GetAsString()); 822 } 823 const DOMHighResTimeStamp startTime = ResolveStartTimeForMeasure( 824 startMark, options, aRv, /* aReturnUnclamped */ false); 825 if (NS_WARN_IF(aRv.Failed())) { 826 return nullptr; 827 } 828 829 JS::Rooted<JS::Value> detail(aCx); 830 if (options && !options->mDetail.isNullOrUndefined()) { 831 StructuredSerializeOptions serializeOptions; 832 JS::Rooted<JS::Value> valueToClone(aCx, options->mDetail); 833 nsContentUtils::StructuredClone(aCx, GetParentObject(), valueToClone, 834 serializeOptions, &detail, aRv); 835 if (aRv.Failed()) { 836 return nullptr; 837 } 838 } else { 839 detail.setNull(); 840 } 841 842 #ifdef MOZ_PERFETTO 843 // Perfetto requires that events are properly nested within each category. 844 // Since this is not a guarantee here, we need to define a dynamic category 845 // for each measurement so it's not prematurely ended by another measurement 846 // that overlaps. We also use the usertiming category to guard these markers 847 // so it's easy to toggle. 848 if (TRACE_EVENT_CATEGORY_ENABLED("usertiming")) { 849 NS_ConvertUTF16toUTF8 str(aName); 850 perfetto::DynamicCategory category{str.get()}; 851 TimeStamp startTimeStamp = 852 CreationTimeStamp() + TimeDuration::FromMilliseconds(startTime); 853 TimeStamp endTimeStamp = 854 CreationTimeStamp() + TimeDuration::FromMilliseconds(endTime); 855 PERFETTO_TRACE_EVENT_BEGIN(category, perfetto::DynamicString{str.get()}, 856 startTimeStamp); 857 PERFETTO_TRACE_EVENT_END(category, endTimeStamp); 858 } 859 #endif 860 861 RefPtr<PerformanceMeasure> performanceMeasure = new PerformanceMeasure( 862 GetParentObject(), aName, startTime, endTime, detail); 863 InsertUserEntry(performanceMeasure); 864 865 MaybeEmitExternalProfilerMarker(aName, options, startMark, aEndMark); 866 867 MaybeAddProfileMarker(aName, options, startMark, aEndMark); 868 869 return performanceMeasure.forget(); 870 } 871 872 void Performance::ClearMeasures(const Optional<nsAString>& aName) { 873 ClearUserEntries(aName, u"measure"_ns); 874 } 875 876 void Performance::LogEntry(PerformanceEntry* aEntry, 877 const nsACString& aOwner) const { 878 PERFLOG("Performance Entry: %s|%s|%s|%f|%f|%" PRIu64 "\n", 879 aOwner.BeginReading(), 880 NS_ConvertUTF16toUTF8(aEntry->GetEntryType()->GetUTF16String()).get(), 881 NS_ConvertUTF16toUTF8(aEntry->GetName()->GetUTF16String()).get(), 882 aEntry->StartTime(), aEntry->Duration(), 883 static_cast<uint64_t>(PR_Now() / PR_USEC_PER_MSEC)); 884 } 885 886 void Performance::TimingNotification(PerformanceEntry* aEntry, 887 const nsACString& aOwner, 888 const double aEpoch) { 889 PerformanceEntryEventInit init; 890 init.mBubbles = false; 891 init.mCancelable = false; 892 aEntry->GetName(init.mName); 893 aEntry->GetEntryType(init.mEntryType); 894 init.mStartTime = aEntry->StartTime(); 895 init.mDuration = aEntry->Duration(); 896 init.mEpoch = aEpoch; 897 CopyUTF8toUTF16(aOwner, init.mOrigin); 898 899 RefPtr<PerformanceEntryEvent> perfEntryEvent = 900 PerformanceEntryEvent::Constructor(this, u"performanceentry"_ns, init); 901 if (RefPtr<nsGlobalWindowInner> owner = GetOwnerWindow()) { 902 owner->DispatchEvent(*perfEntryEvent); 903 } 904 } 905 906 void Performance::InsertUserEntry(PerformanceEntry* aEntry) { 907 mUserEntries.InsertElementSorted(aEntry, PerformanceEntryComparator()); 908 909 QueueEntry(aEntry); 910 } 911 912 /* 913 * Steps are labeled according to the description found at 914 * https://w3c.github.io/resource-timing/#sec-extensions-performance-interface. 915 * 916 * Buffer Full Event 917 */ 918 void Performance::ResourceTimingBufferFullEvent() { 919 /* 920 * While resource timing secondary buffer is not empty, 921 * run the following substeps: 922 */ 923 while (!mSecondaryResourceEntries.IsEmpty()) { 924 uint32_t secondaryResourceEntriesBeforeCount = 0; 925 uint32_t secondaryResourceEntriesAfterCount = 0; 926 927 /* 928 * Let number of excess entries before be resource 929 * timing secondary buffer current size. 930 */ 931 secondaryResourceEntriesBeforeCount = mSecondaryResourceEntries.Length(); 932 933 /* 934 * If can add resource timing entry returns false, 935 * then fire an event named resourcetimingbufferfull 936 * at the Performance object. 937 */ 938 if (!CanAddResourceTimingEntry()) { 939 DispatchResourceTimingBufferFullEvent(); 940 } 941 942 /* 943 * Run copy secondary buffer. 944 * 945 * While resource timing secondary buffer is not 946 * empty and can add resource timing entry returns 947 * true ... 948 */ 949 while (!mSecondaryResourceEntries.IsEmpty() && 950 CanAddResourceTimingEntry()) { 951 /* 952 * Let entry be the oldest PerformanceResourceTiming 953 * in resource timing secondary buffer. Add entry to 954 * the end of performance entry buffer. Increment 955 * resource timing buffer current size by 1. 956 */ 957 mResourceEntries.InsertElementSorted( 958 mSecondaryResourceEntries.ElementAt(0), PerformanceEntryComparator()); 959 /* 960 * Remove entry from resource timing secondary buffer. 961 * Decrement resource timing secondary buffer current 962 * size by 1. 963 */ 964 mSecondaryResourceEntries.RemoveElementAt(0); 965 } 966 967 /* 968 * Let number of excess entries after be resource 969 * timing secondary buffer current size. 970 */ 971 secondaryResourceEntriesAfterCount = mSecondaryResourceEntries.Length(); 972 973 /* 974 * If number of excess entries before is lower than 975 * or equals number of excess entries after, then 976 * remove all entries from resource timing secondary 977 * buffer, set resource timing secondary buffer current 978 * size to 0, and abort these steps. 979 */ 980 if (secondaryResourceEntriesBeforeCount <= 981 secondaryResourceEntriesAfterCount) { 982 mSecondaryResourceEntries.Clear(); 983 break; 984 } 985 } 986 /* 987 * Set resource timing buffer full event pending flag 988 * to false. 989 */ 990 mPendingResourceTimingBufferFullEvent = false; 991 } 992 993 void Performance::SetResourceTimingBufferSize(uint64_t aMaxSize) { 994 mResourceTimingBufferSize = aMaxSize; 995 } 996 997 /* 998 * Steps are labeled according to the description found at 999 * https://w3c.github.io/resource-timing/#sec-extensions-performance-interface. 1000 * 1001 * Can Add Resource Timing Entry 1002 */ 1003 MOZ_ALWAYS_INLINE bool Performance::CanAddResourceTimingEntry() { 1004 /* 1005 * If resource timing buffer current size is smaller than resource timing 1006 * buffer size limit, return true. [Otherwise,] [r]eturn false. 1007 */ 1008 return mResourceEntries.Length() < mResourceTimingBufferSize; 1009 } 1010 1011 /* 1012 * Steps are labeled according to the description found at 1013 * https://w3c.github.io/resource-timing/#sec-extensions-performance-interface. 1014 * 1015 * Add a PerformanceResourceTiming Entry 1016 */ 1017 void Performance::InsertResourceEntry(PerformanceEntry* aEntry) { 1018 MOZ_ASSERT(aEntry); 1019 1020 QueueEntry(aEntry); 1021 1022 /* 1023 * Let new entry be the input PerformanceEntry to be added. 1024 * 1025 * If can add resource timing entry returns true and resource 1026 * timing buffer full event pending flag is false ... 1027 */ 1028 if (CanAddResourceTimingEntry() && !mPendingResourceTimingBufferFullEvent) { 1029 /* 1030 * Add new entry to the performance entry buffer. 1031 * Increase resource timing buffer current size by 1. 1032 */ 1033 mResourceEntries.InsertElementSorted(aEntry, PerformanceEntryComparator()); 1034 return; 1035 } 1036 1037 /* 1038 * If resource timing buffer full event pending flag is 1039 * false ... 1040 */ 1041 if (!mPendingResourceTimingBufferFullEvent) { 1042 /* 1043 * Set resource timing buffer full event pending flag 1044 * to true. 1045 */ 1046 mPendingResourceTimingBufferFullEvent = true; 1047 1048 /* 1049 * Queue a task to run fire a buffer full event. 1050 */ 1051 NS_DispatchToCurrentThread(NewCancelableRunnableMethod( 1052 "Performance::ResourceTimingBufferFullEvent", this, 1053 &Performance::ResourceTimingBufferFullEvent)); 1054 } 1055 /* 1056 * Add new entry to the resource timing secondary buffer. 1057 * Increase resource timing secondary buffer current size 1058 * by 1. 1059 */ 1060 mSecondaryResourceEntries.InsertElementSorted(aEntry, 1061 PerformanceEntryComparator()); 1062 } 1063 1064 void Performance::AddObserver(PerformanceObserver* aObserver) { 1065 mObservers.AppendElementUnlessExists(aObserver); 1066 } 1067 1068 void Performance::RemoveObserver(PerformanceObserver* aObserver) { 1069 mObservers.RemoveElement(aObserver); 1070 } 1071 1072 void Performance::NotifyObservers() { 1073 mPendingNotificationObserversTask = false; 1074 NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mObservers, Notify, ()); 1075 } 1076 1077 void Performance::CancelNotificationObservers() { 1078 mPendingNotificationObserversTask = false; 1079 } 1080 1081 class NotifyObserversTask final : public CancelableRunnable { 1082 public: 1083 explicit NotifyObserversTask(Performance* aPerformance) 1084 : CancelableRunnable("dom::NotifyObserversTask"), 1085 mPerformance(aPerformance) { 1086 MOZ_ASSERT(mPerformance); 1087 } 1088 1089 // MOZ_CAN_RUN_SCRIPT_BOUNDARY for now until Runnable::Run is 1090 // MOZ_CAN_RUN_SCRIPT. 1091 MOZ_CAN_RUN_SCRIPT_BOUNDARY 1092 NS_IMETHOD Run() override { 1093 MOZ_ASSERT(mPerformance); 1094 RefPtr<Performance> performance(mPerformance); 1095 performance->NotifyObservers(); 1096 return NS_OK; 1097 } 1098 1099 nsresult Cancel() override { 1100 mPerformance->CancelNotificationObservers(); 1101 mPerformance = nullptr; 1102 return NS_OK; 1103 } 1104 1105 private: 1106 ~NotifyObserversTask() = default; 1107 1108 RefPtr<Performance> mPerformance; 1109 }; 1110 1111 void Performance::QueueNotificationObserversTask() { 1112 if (!mPendingNotificationObserversTask) { 1113 RunNotificationObserversTask(); 1114 } 1115 } 1116 1117 void Performance::RunNotificationObserversTask() { 1118 mPendingNotificationObserversTask = true; 1119 nsCOMPtr<nsIRunnable> task = new NotifyObserversTask(this); 1120 nsresult rv; 1121 if (nsIGlobalObject* global = GetOwnerGlobal()) { 1122 rv = global->Dispatch(task.forget()); 1123 } else { 1124 rv = NS_DispatchToCurrentThread(task.forget()); 1125 } 1126 if (NS_WARN_IF(NS_FAILED(rv))) { 1127 mPendingNotificationObserversTask = false; 1128 } 1129 } 1130 1131 void Performance::QueueEntry(PerformanceEntry* aEntry) { 1132 nsTObserverArray<PerformanceObserver*> interestedObservers; 1133 if (!mObservers.IsEmpty()) { 1134 const auto [begin, end] = mObservers.NonObservingRange(); 1135 std::copy_if(begin, end, MakeBackInserter(interestedObservers), 1136 [&](PerformanceObserver* observer) { 1137 return observer->ObservesTypeOfEntry(aEntry); 1138 }); 1139 } 1140 1141 NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(interestedObservers, QueueEntry, 1142 (aEntry)); 1143 1144 aEntry->BufferEntryIfNeeded(); 1145 1146 if (!interestedObservers.IsEmpty()) { 1147 QueueNotificationObserversTask(); 1148 } 1149 } 1150 1151 // We could clear User entries here, but doing so could break sites that call 1152 // performance.measure() if the marks disappeared without warning. Chrome 1153 // allows "infinite" entries. 1154 void Performance::MemoryPressure() {} 1155 1156 size_t Performance::SizeOfUserEntries( 1157 mozilla::MallocSizeOf aMallocSizeOf) const { 1158 size_t userEntries = 0; 1159 for (const PerformanceEntry* entry : mUserEntries) { 1160 userEntries += entry->SizeOfIncludingThis(aMallocSizeOf); 1161 } 1162 return userEntries; 1163 } 1164 1165 size_t Performance::SizeOfResourceEntries( 1166 mozilla::MallocSizeOf aMallocSizeOf) const { 1167 size_t resourceEntries = 0; 1168 for (const PerformanceEntry* entry : mResourceEntries) { 1169 resourceEntries += entry->SizeOfIncludingThis(aMallocSizeOf); 1170 } 1171 return resourceEntries; 1172 } 1173 1174 } // namespace mozilla::dom