summaryrefslogtreecommitdiffstats
path: root/dom/performance
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/performance/EventCounts.cpp79
-rw-r--r--dom/performance/EventCounts.h31
-rw-r--r--dom/performance/LargestContentfulPaint.cpp565
-rw-r--r--dom/performance/LargestContentfulPaint.h236
-rw-r--r--dom/performance/Performance.cpp1045
-rw-r--r--dom/performance/Performance.h259
-rw-r--r--dom/performance/PerformanceEntry.cpp54
-rw-r--r--dom/performance/PerformanceEntry.h104
-rw-r--r--dom/performance/PerformanceEventTiming.cpp207
-rw-r--r--dom/performance/PerformanceEventTiming.h136
-rw-r--r--dom/performance/PerformanceMainThread.cpp756
-rw-r--r--dom/performance/PerformanceMainThread.h234
-rw-r--r--dom/performance/PerformanceMark.cpp124
-rw-r--r--dom/performance/PerformanceMark.h68
-rw-r--r--dom/performance/PerformanceMeasure.cpp62
-rw-r--r--dom/performance/PerformanceMeasure.h49
-rw-r--r--dom/performance/PerformanceNavigation.cpp31
-rw-r--r--dom/performance/PerformanceNavigation.h50
-rw-r--r--dom/performance/PerformanceNavigationTiming.cpp156
-rw-r--r--dom/performance/PerformanceNavigationTiming.h93
-rw-r--r--dom/performance/PerformanceObserver.cpp384
-rw-r--r--dom/performance/PerformanceObserver.h91
-rw-r--r--dom/performance/PerformanceObserverEntryList.cpp100
-rw-r--r--dom/performance/PerformanceObserverEntryList.h55
-rw-r--r--dom/performance/PerformancePaintTiming.cpp52
-rw-r--r--dom/performance/PerformancePaintTiming.h50
-rw-r--r--dom/performance/PerformanceResourceTiming.cpp133
-rw-r--r--dom/performance/PerformanceResourceTiming.h172
-rw-r--r--dom/performance/PerformanceServerTiming.cpp71
-rw-r--r--dom/performance/PerformanceServerTiming.h52
-rw-r--r--dom/performance/PerformanceService.cpp42
-rw-r--r--dom/performance/PerformanceService.h43
-rw-r--r--dom/performance/PerformanceStorage.h35
-rw-r--r--dom/performance/PerformanceStorageWorker.cpp185
-rw-r--r--dom/performance/PerformanceStorageWorker.h48
-rw-r--r--dom/performance/PerformanceTiming.cpp677
-rw-r--r--dom/performance/PerformanceTiming.h595
-rw-r--r--dom/performance/PerformanceTimingTypes.ipdlh50
-rw-r--r--dom/performance/PerformanceWorker.cpp61
-rw-r--r--dom/performance/PerformanceWorker.h96
-rw-r--r--dom/performance/metrics.yaml32
-rw-r--r--dom/performance/moz.build65
-rw-r--r--dom/performance/tests/empty.js1
-rw-r--r--dom/performance/tests/logo.pngbin0 -> 21901 bytes
-rw-r--r--dom/performance/tests/mochitest.toml67
-rw-r--r--dom/performance/tests/serverTiming.sjs41
-rw-r--r--dom/performance/tests/sharedworker_performance_user_timing.js38
-rw-r--r--dom/performance/tests/test_performance_navigation_timing.html104
-rw-r--r--dom/performance/tests/test_performance_observer.html142
-rw-r--r--dom/performance/tests/test_performance_observer.js286
-rw-r--r--dom/performance/tests/test_performance_paint_observer.html40
-rw-r--r--dom/performance/tests/test_performance_paint_observer_helper.html35
-rw-r--r--dom/performance/tests/test_performance_paint_timing.html38
-rw-r--r--dom/performance/tests/test_performance_paint_timing_helper.html65
-rw-r--r--dom/performance/tests/test_performance_server_timing.html58
-rw-r--r--dom/performance/tests/test_performance_server_timing_plain_http.html42
-rw-r--r--dom/performance/tests/test_performance_timing_json.html32
-rw-r--r--dom/performance/tests/test_performance_user_timing.html49
-rw-r--r--dom/performance/tests/test_performance_user_timing.js318
-rw-r--r--dom/performance/tests/test_performance_user_timing_dying_global.html61
-rw-r--r--dom/performance/tests/test_sharedWorker_performance_user_timing.html30
-rw-r--r--dom/performance/tests/test_timeOrigin.html76
-rw-r--r--dom/performance/tests/test_worker_observer.html41
-rw-r--r--dom/performance/tests/test_worker_performance_entries.html39
-rw-r--r--dom/performance/tests/test_worker_performance_entries.js120
-rw-r--r--dom/performance/tests/test_worker_performance_entries.sjs11
-rw-r--r--dom/performance/tests/test_worker_performance_now.html31
-rw-r--r--dom/performance/tests/test_worker_performance_now.js68
-rw-r--r--dom/performance/tests/test_worker_user_timing.html30
-rw-r--r--dom/performance/tests/worker_performance_observer.js4
-rw-r--r--dom/performance/tests/worker_performance_user_timing.js32
71 files changed, 9327 insertions, 0 deletions
diff --git a/dom/performance/EventCounts.cpp b/dom/performance/EventCounts.cpp
new file mode 100644
index 0000000000..b94545e913
--- /dev/null
+++ b/dom/performance/EventCounts.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "nsIGlobalObject.h"
+#include "EventCounts.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventCounts.h"
+#include "mozilla/dom/PerformanceEventTimingBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(EventCounts, mParent)
+
+static const EventMessage sQualifiedEventType[36] = {
+ EventMessage::eMouseAuxClick,
+ EventMessage::eMouseClick,
+ EventMessage::eContextMenu,
+ EventMessage::eMouseDoubleClick,
+ EventMessage::eMouseDown,
+ EventMessage::eMouseEnter,
+ EventMessage::eMouseLeave,
+ EventMessage::eMouseOut,
+ EventMessage::eMouseOver,
+ EventMessage::eMouseUp,
+ EventMessage::ePointerOver,
+ EventMessage::ePointerEnter,
+ EventMessage::ePointerDown,
+ EventMessage::ePointerUp,
+ EventMessage::ePointerCancel,
+ EventMessage::ePointerOut,
+ EventMessage::ePointerLeave,
+ EventMessage::ePointerGotCapture,
+ EventMessage::ePointerLostCapture,
+ EventMessage::eTouchStart,
+ EventMessage::eTouchEnd,
+ EventMessage::eTouchCancel,
+ EventMessage::eKeyDown,
+ EventMessage::eKeyPress,
+ EventMessage::eKeyUp,
+ EventMessage::eEditorBeforeInput,
+ EventMessage::eEditorInput,
+ EventMessage::eCompositionStart,
+ EventMessage::eCompositionUpdate,
+ EventMessage::eCompositionEnd,
+ EventMessage::eDragStart,
+ EventMessage::eDragEnd,
+ EventMessage::eDragEnter,
+ EventMessage::eDragLeave,
+ EventMessage::eDragOver,
+ EventMessage::eDrop};
+
+EventCounts::EventCounts(nsISupports* aParent) : mParent(aParent) {
+ ErrorResult rv;
+
+ for (const EventMessage& eventType : sQualifiedEventType) {
+ EventCounts_Binding::MaplikeHelpers::Set(
+ this, nsDependentString(Event::GetEventName(eventType)), 0, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+#ifdef DEBUG
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
+ if (global) {
+ MOZ_ASSERT(global->IsDying());
+ }
+#endif
+ return;
+ }
+ }
+}
+
+JSObject* EventCounts::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return EventCounts_Binding::Wrap(aCx, this, aGivenProto);
+}
+} // namespace mozilla::dom
diff --git a/dom/performance/EventCounts.h b/dom/performance/EventCounts.h
new file mode 100644
index 0000000000..59d9c140fc
--- /dev/null
+++ b/dom/performance/EventCounts.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceEventCounts_h___
+#define mozilla_dom_PerformanceEventCounts_h___
+
+#include "nsCOMPtr.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+class EventCounts final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EventCounts)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(EventCounts)
+
+ explicit EventCounts(nsISupports* aParent);
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ ~EventCounts() = default;
+ nsCOMPtr<nsISupports> mParent;
+};
+} // namespace mozilla::dom
+#endif
diff --git a/dom/performance/LargestContentfulPaint.cpp b/dom/performance/LargestContentfulPaint.cpp
new file mode 100644
index 0000000000..3382a2fbad
--- /dev/null
+++ b/dom/performance/LargestContentfulPaint.cpp
@@ -0,0 +1,565 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/dom/Element.h"
+#include "nsContentUtils.h"
+#include "nsLayoutUtils.h"
+#include "nsRFPService.h"
+#include "Performance.h"
+#include "imgRequest.h"
+#include "PerformanceMainThread.h"
+#include "LargestContentfulPaint.h"
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/DOMIntersectionObserver.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+
+#include "mozilla/PresShell.h"
+#include "mozilla/Logging.h"
+#include "mozilla/nsVideoFrame.h"
+
+namespace mozilla::dom {
+
+static LazyLogModule gLCPLogging("LargestContentfulPaint");
+
+#define LOG(...) MOZ_LOG(gLCPLogging, LogLevel::Debug, (__VA_ARGS__))
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(LargestContentfulPaint, PerformanceEntry,
+ mPerformance, mURI, mElement)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LargestContentfulPaint)
+NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry)
+
+NS_IMPL_ADDREF_INHERITED(LargestContentfulPaint, PerformanceEntry)
+NS_IMPL_RELEASE_INHERITED(LargestContentfulPaint, PerformanceEntry)
+
+static double GetAreaInDoublePixelsFromAppUnits(const nsSize& aSize) {
+ return NSAppUnitsToDoublePixels(aSize.Width(), AppUnitsPerCSSPixel()) *
+ NSAppUnitsToDoublePixels(aSize.Height(), AppUnitsPerCSSPixel());
+}
+
+static double GetAreaInDoublePixelsFromAppUnits(const nsRect& aRect) {
+ return NSAppUnitsToDoublePixels(aRect.Width(), AppUnitsPerCSSPixel()) *
+ NSAppUnitsToDoublePixels(aRect.Height(), AppUnitsPerCSSPixel());
+}
+
+static DOMHighResTimeStamp GetReducedTimePrecisionDOMHighRes(
+ Performance* aPerformance, const TimeStamp& aRawTimeStamp) {
+ MOZ_ASSERT(aPerformance);
+ DOMHighResTimeStamp rawValue =
+ aPerformance->GetDOMTiming()->TimeStampToDOMHighRes(aRawTimeStamp);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+ImagePendingRendering::ImagePendingRendering(
+ const LCPImageEntryKey& aLCPImageEntryKey, const TimeStamp& aLoadTime)
+ : mLCPImageEntryKey(aLCPImageEntryKey), mLoadTime(aLoadTime) {}
+
+LargestContentfulPaint::LargestContentfulPaint(
+ PerformanceMainThread* aPerformance, const TimeStamp& aRenderTime,
+ const Maybe<TimeStamp>& aLoadTime, const unsigned long aSize, nsIURI* aURI,
+ Element* aElement, const Maybe<const LCPImageEntryKey>& aLCPImageEntryKey,
+ bool aShouldExposeRenderTime)
+ : PerformanceEntry(aPerformance->GetParentObject(), u""_ns,
+ kLargestContentfulPaintName),
+ mPerformance(aPerformance),
+ mRenderTime(aRenderTime),
+ mLoadTime(aLoadTime),
+ mShouldExposeRenderTime(aShouldExposeRenderTime),
+ mSize(aSize),
+ mURI(aURI),
+ mLCPImageEntryKey(aLCPImageEntryKey) {
+ MOZ_ASSERT(mPerformance);
+ MOZ_ASSERT(aElement);
+ // The element could be a pseudo-element
+ if (aElement->ChromeOnlyAccess()) {
+ mElement = do_GetWeakReference(Element::FromNodeOrNull(
+ aElement->FindFirstNonChromeOnlyAccessContent()));
+ } else {
+ mElement = do_GetWeakReference(aElement);
+ }
+
+ if (const Element* element = GetElement()) {
+ mId = element->GetID();
+ }
+}
+
+JSObject* LargestContentfulPaint::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return LargestContentfulPaint_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+Element* LargestContentfulPaint::GetElement() const {
+ nsCOMPtr<Element> element = do_QueryReferent(mElement);
+ return element ? nsContentUtils::GetAnElementForTiming(
+ element, element->GetComposedDoc(), nullptr)
+ : nullptr;
+}
+
+void LargestContentfulPaint::BufferEntryIfNeeded() {
+ MOZ_ASSERT(mLCPImageEntryKey.isNothing());
+ mPerformance->BufferLargestContentfulPaintEntryIfNeeded(this);
+}
+
+/* static*/
+bool LCPHelpers::IsQualifiedImageRequest(imgRequest* aRequest,
+ Element* aContainingElement) {
+ MOZ_ASSERT(aContainingElement);
+ if (!aRequest) {
+ return false;
+ }
+
+ if (aRequest->IsChrome()) {
+ return false;
+ }
+
+ if (!aContainingElement->ChromeOnlyAccess()) {
+ return true;
+ }
+
+ // Exception: this is a poster image of video element
+ if (nsIContent* parent = aContainingElement->GetParent()) {
+ nsVideoFrame* videoFrame = do_QueryFrame(parent->GetPrimaryFrame());
+ if (videoFrame && videoFrame->GetPosterImage() == aContainingElement) {
+ return true;
+ }
+ }
+
+ // Exception: CSS generated images
+ if (aContainingElement->IsInNativeAnonymousSubtree()) {
+ if (nsINode* rootParentOrHost =
+ aContainingElement
+ ->GetClosestNativeAnonymousSubtreeRootParentOrHost()) {
+ if (!rootParentOrHost->ChromeOnlyAccess()) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+void LargestContentfulPaint::MaybeProcessImageForElementTiming(
+ imgRequestProxy* aRequest, Element* aElement) {
+ if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
+ return;
+ }
+
+ MOZ_ASSERT(aRequest);
+ imgRequest* request = aRequest->GetOwner();
+ if (!LCPHelpers::IsQualifiedImageRequest(request, aElement)) {
+ return;
+ }
+
+ Document* document = aElement->GetComposedDoc();
+ if (!document) {
+ return;
+ }
+
+ nsPresContext* pc =
+ aElement->GetPresContext(Element::PresContextFor::eForComposedDoc);
+ if (!pc) {
+ return;
+ }
+
+ PerformanceMainThread* performance = pc->GetPerformanceMainThread();
+ if (!performance) {
+ return;
+ }
+
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging, LogLevel::Debug))) {
+ nsCOMPtr<nsIURI> uri;
+ aRequest->GetURI(getter_AddRefs(uri));
+ LOG("MaybeProcessImageForElementTiming, Element=%p, URI=%s, "
+ "performance=%p ",
+ aElement, uri ? uri->GetSpecOrDefault().get() : "", performance);
+ }
+
+ const LCPImageEntryKey entryKey = LCPImageEntryKey(aElement, aRequest);
+ if (!document->ContentIdentifiersForLCP().EnsureInserted(entryKey)) {
+ LOG(" The content identifier existed for element=%p and request=%p, "
+ "return.",
+ aElement, aRequest);
+ return;
+ }
+
+#ifdef DEBUG
+ uint32_t status = imgIRequest::STATUS_NONE;
+ aRequest->GetImageStatus(&status);
+ MOZ_ASSERT(status & imgIRequest::STATUS_LOAD_COMPLETE);
+#endif
+
+ // At this point, the loadTime of the image is known, but
+ // the renderTime is unknown, so it's added to ImagesPendingRendering
+ // as a placeholder, and the corresponding LCP entry will be created
+ // when the renderTime is known.
+ // Here we are exposing the load time of the image which could be
+ // a privacy concern. The spec talks about it at
+ // https://wicg.github.io/element-timing/#sec-security
+ // TLDR: The similar metric can be obtained by ResourceTiming
+ // API and onload handlers already, so this is not exposing anything
+ // new.
+ LOG(" Added a pending image rendering");
+ performance->AddImagesPendingRendering(
+ ImagePendingRendering{entryKey, TimeStamp::Now()});
+}
+
+bool LCPHelpers::CanFinalizeLCPEntry(const nsIFrame* aFrame) {
+ if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
+ return false;
+ }
+
+ if (!aFrame) {
+ return false;
+ }
+
+ nsPresContext* presContext = aFrame->PresContext();
+ return !presContext->HasStoppedGeneratingLCP() &&
+ presContext->GetPerformanceMainThread();
+}
+
+void LCPHelpers::FinalizeLCPEntryForImage(
+ Element* aContainingBlock, imgRequestProxy* aImgRequestProxy,
+ const nsRect& aTargetRectRelativeToSelf) {
+ LOG("FinalizeLCPEntryForImage element=%p", aContainingBlock);
+ if (!aImgRequestProxy) {
+ return;
+ }
+
+ if (!IsQualifiedImageRequest(aImgRequestProxy->GetOwner(),
+ aContainingBlock)) {
+ return;
+ }
+
+ nsIFrame* frame = aContainingBlock->GetPrimaryFrame();
+
+ if (!CanFinalizeLCPEntry(frame)) {
+ return;
+ }
+
+ PerformanceMainThread* performance =
+ frame->PresContext()->GetPerformanceMainThread();
+ MOZ_ASSERT(performance);
+
+ RefPtr<LargestContentfulPaint> entry =
+ performance->GetImageLCPEntry(aContainingBlock, aImgRequestProxy);
+ if (!entry) {
+ LOG(" No Image Entry");
+ return;
+ }
+ entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, performance,
+ true);
+ // If area is less than or equal to document’s largest contentful paint size,
+ // return.
+ if (!performance->UpdateLargestContentfulPaintSize(entry->Size())) {
+ LOG(
+
+ " This paint(%lu) is not greater than the largest paint (%lf)that "
+ "we've "
+ "reported so far, return",
+ entry->Size(), performance->GetLargestContentfulPaintSize());
+ return;
+ }
+
+ entry->QueueEntry();
+}
+
+DOMHighResTimeStamp LargestContentfulPaint::RenderTime() const {
+ if (!mShouldExposeRenderTime) {
+ return 0;
+ }
+ return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
+}
+
+DOMHighResTimeStamp LargestContentfulPaint::LoadTime() const {
+ if (mLoadTime.isNothing()) {
+ return 0;
+ }
+
+ return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
+}
+
+DOMHighResTimeStamp LargestContentfulPaint::StartTime() const {
+ if (mShouldExposeRenderTime) {
+ return GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime);
+ }
+
+ if (mLoadTime.isNothing()) {
+ return 0;
+ }
+
+ return GetReducedTimePrecisionDOMHighRes(mPerformance, mLoadTime.ref());
+}
+
+/* static */
+Element* LargestContentfulPaint::GetContainingBlockForTextFrame(
+ const nsTextFrame* aTextFrame) {
+ nsIFrame* containingFrame = aTextFrame->GetContainingBlock();
+ MOZ_ASSERT(containingFrame);
+ return Element::FromNodeOrNull(containingFrame->GetContent());
+}
+
+void LargestContentfulPaint::QueueEntry() {
+ LOG("QueueEntry entry=%p", this);
+ mPerformance->QueueLargestContentfulPaintEntry(this);
+
+ ReportLCPToNavigationTimings();
+}
+
+void LargestContentfulPaint::GetUrl(nsAString& aUrl) {
+ if (mURI) {
+ CopyUTF8toUTF16(mURI->GetSpecOrDefault(), aUrl);
+ }
+}
+
+void LargestContentfulPaint::UpdateSize(
+ const Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf,
+ const PerformanceMainThread* aPerformance, bool aIsImage) {
+ nsIFrame* frame = aContainingBlock->GetPrimaryFrame();
+ MOZ_ASSERT(frame);
+
+ nsIFrame* rootFrame = frame->PresShell()->GetRootFrame();
+ if (!rootFrame) {
+ return;
+ }
+
+ if (frame->Style()->IsInOpacityZeroSubtree()) {
+ LOG(" Opacity:0 return");
+ return;
+ }
+
+ // The following size computation is based on a pending pull request
+ // https://github.com/w3c/largest-contentful-paint/pull/99
+
+ // Let visibleDimensions be concreteDimensions, adjusted for positioning
+ // by object-position or background-position and element’s content box.
+ const nsRect& visibleDimensions = aTargetRectRelativeToSelf;
+
+ // Let clientContentRect be the smallest DOMRectReadOnly containing
+ // visibleDimensions with element’s transforms applied.
+ nsRect clientContentRect = nsLayoutUtils::TransformFrameRectToAncestor(
+ frame, visibleDimensions, rootFrame);
+
+ // Let intersectionRect be the value returned by the intersection rect
+ // algorithm using element as the target and viewport as the root.
+ // (From https://wicg.github.io/element-timing/#sec-report-image-element)
+ IntersectionInput input = DOMIntersectionObserver::ComputeInput(
+ *frame->PresContext()->Document(), rootFrame->GetContent(), nullptr);
+ const IntersectionOutput output =
+ DOMIntersectionObserver::Intersect(input, *aContainingBlock);
+
+ Maybe<nsRect> intersectionRect = output.mIntersectionRect;
+
+ if (intersectionRect.isNothing()) {
+ LOG(" The intersectionRect is nothing for Element=%p. return.",
+ aContainingBlock);
+ return;
+ }
+
+ // Let intersectingClientContentRect be the intersection of clientContentRect
+ // with intersectionRect.
+ Maybe<nsRect> intersectionWithContentRect =
+ clientContentRect.EdgeInclusiveIntersection(intersectionRect.value());
+
+ if (intersectionWithContentRect.isNothing()) {
+ LOG(" The intersectionWithContentRect is nothing for Element=%p. return.",
+ aContainingBlock);
+ return;
+ }
+
+ nsRect renderedRect = intersectionWithContentRect.value();
+
+ double area = GetAreaInDoublePixelsFromAppUnits(renderedRect);
+
+ double viewport = GetAreaInDoublePixelsFromAppUnits(input.mRootRect);
+
+ LOG(" Viewport = %f, RenderRect = %f.", viewport, area);
+ // We don't want to report things that take the entire viewport.
+ if (area >= viewport) {
+ LOG(" The renderedRect is at least same as the area of the "
+ "viewport for Element=%p, return.",
+ aContainingBlock);
+ return;
+ }
+
+ Maybe<nsSize> intrinsicSize = frame->GetIntrinsicSize().ToSize();
+ const bool hasIntrinsicSize = intrinsicSize && !intrinsicSize->IsEmpty();
+
+ if (aIsImage && hasIntrinsicSize) {
+ // Let (naturalWidth, naturalHeight) be imageRequest’s natural dimension.
+ // Let naturalArea be naturalWidth * naturalHeight.
+ double naturalArea =
+ GetAreaInDoublePixelsFromAppUnits(intrinsicSize.value());
+
+ LOG(" naturalArea = %f", naturalArea);
+
+ // Let boundingClientArea be clientContentRect’s width * clientContentRect’s
+ // height.
+ double boundingClientArea =
+ NSAppUnitsToDoublePixels(clientContentRect.Width(),
+ AppUnitsPerCSSPixel()) *
+ NSAppUnitsToDoublePixels(clientContentRect.Height(),
+ AppUnitsPerCSSPixel());
+ LOG(" boundingClientArea = %f", boundingClientArea);
+
+ // Let scaleFactor be boundingClientArea / naturalArea.
+ double scaleFactor = boundingClientArea / naturalArea;
+ LOG(" scaleFactor = %f", scaleFactor);
+
+ // If scaleFactor is greater than 1, then divide area by scaleFactor.
+ if (scaleFactor > 1) {
+ LOG(" area before sacled doown %f", area);
+ area = area / scaleFactor;
+ }
+ }
+
+ MOZ_ASSERT(!mSize);
+ mSize = area;
+}
+
+void LCPTextFrameHelper::MaybeUnionTextFrame(
+ nsTextFrame* aTextFrame, const nsRect& aRelativeToSelfRect) {
+ if (!StaticPrefs::dom_enable_largest_contentful_paint() ||
+ aTextFrame->PresContext()->HasStoppedGeneratingLCP()) {
+ return;
+ }
+
+ Element* containingBlock =
+ LargestContentfulPaint::GetContainingBlockForTextFrame(aTextFrame);
+ if (!containingBlock ||
+ // If element is contained in doc’s set of elements with rendered text,
+ // continue
+ containingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT) ||
+ containingBlock->ChromeOnlyAccess()) {
+ return;
+ }
+
+ MOZ_ASSERT(containingBlock->GetPrimaryFrame());
+
+ PerformanceMainThread* perf =
+ aTextFrame->PresContext()->GetPerformanceMainThread();
+ if (!perf) {
+ return;
+ }
+
+ auto& unionRect = perf->GetTextFrameUnions().LookupOrInsert(containingBlock);
+ unionRect = unionRect.Union(aRelativeToSelfRect);
+}
+
+void LCPHelpers::CreateLCPEntryForImage(
+ PerformanceMainThread* aPerformance, Element* aElement,
+ imgRequestProxy* aRequestProxy, const TimeStamp& aLoadTime,
+ const TimeStamp& aRenderTime, const LCPImageEntryKey& aImageEntryKey) {
+ MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint());
+ MOZ_ASSERT(aRequestProxy);
+ MOZ_ASSERT(aPerformance);
+ if (MOZ_UNLIKELY(MOZ_LOG_TEST(gLCPLogging, LogLevel::Debug))) {
+ nsCOMPtr<nsIURI> uri;
+ aRequestProxy->GetURI(getter_AddRefs(uri));
+ LOG("CreateLCPEntryForImage "
+ "Element=%p, aRequestProxy=%p, URI=%s loadTime=%f, "
+ "aRenderTime=%f\n",
+ aElement, aRequestProxy, uri->GetSpecOrDefault().get(),
+ GetReducedTimePrecisionDOMHighRes(aPerformance, aLoadTime),
+ GetReducedTimePrecisionDOMHighRes(aPerformance, aRenderTime));
+ }
+ if (aPerformance->HasDispatchedInputEvent() ||
+ aPerformance->HasDispatchedScrollEvent()) {
+ return;
+ }
+
+ // Let url be the empty string.
+ // If imageRequest is not null, set url to be imageRequest’s request URL.
+ nsCOMPtr<nsIURI> requestURI;
+ aRequestProxy->GetURI(getter_AddRefs(requestURI));
+
+ imgRequest* request = aRequestProxy->GetOwner();
+ // We should never get here unless request is valid.
+ MOZ_ASSERT(request);
+
+ bool taoPassed = request->ShouldReportRenderTimeForLCP() || request->IsData();
+ // https://wicg.github.io/element-timing/#report-image-element-timing
+ // For TAO failed requests, the renderTime is exposed as 0 for
+ // security reasons.
+ //
+ // At this point, we have all the information about the entry
+ // except the size.
+ RefPtr<LargestContentfulPaint> entry = new LargestContentfulPaint(
+ aPerformance, aRenderTime, Some(aLoadTime), 0, requestURI, aElement,
+ Some(aImageEntryKey), taoPassed);
+
+ LOG(" Upsert a LargestContentfulPaint entry=%p to LCPEntryMap.",
+ entry.get());
+ aPerformance->StoreImageLCPEntry(aElement, aRequestProxy, entry);
+}
+
+void LCPHelpers::FinalizeLCPEntryForText(
+ PerformanceMainThread* aPerformance, const TimeStamp& aRenderTime,
+ Element* aContainingBlock, const nsRect& aTargetRectRelativeToSelf,
+ const nsPresContext* aPresContext) {
+ MOZ_ASSERT(aPerformance);
+ LOG("FinalizeLCPEntryForText element=%p", aContainingBlock);
+
+ if (!aContainingBlock->GetPrimaryFrame()) {
+ return;
+ }
+ MOZ_ASSERT(CanFinalizeLCPEntry(aContainingBlock->GetPrimaryFrame()));
+ MOZ_ASSERT(!aContainingBlock->HasFlag(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT));
+ MOZ_ASSERT(!aContainingBlock->ChromeOnlyAccess());
+
+ aContainingBlock->SetFlags(ELEMENT_PROCESSED_BY_LCP_FOR_TEXT);
+
+ RefPtr<LargestContentfulPaint> entry =
+ new LargestContentfulPaint(aPerformance, aRenderTime, Nothing(), 0,
+ nullptr, aContainingBlock, Nothing(), true);
+
+ entry->UpdateSize(aContainingBlock, aTargetRectRelativeToSelf, aPerformance,
+ false);
+ // If area is less than or equal to document’s largest contentful paint size,
+ // return.
+ if (!aPerformance->UpdateLargestContentfulPaintSize(entry->Size())) {
+ LOG(" This paint(%lu) is not greater than the largest paint (%lf)that "
+ "we've "
+ "reported so far, return",
+ entry->Size(), aPerformance->GetLargestContentfulPaintSize());
+ return;
+ }
+ entry->QueueEntry();
+}
+
+void LargestContentfulPaint::ReportLCPToNavigationTimings() {
+ nsCOMPtr<Element> element = do_QueryReferent(mElement);
+ if (!element) {
+ return;
+ }
+
+ const Document* document = element->OwnerDoc();
+
+ MOZ_ASSERT(document);
+
+ nsDOMNavigationTiming* timing = document->GetNavigationTiming();
+
+ if (MOZ_UNLIKELY(!timing)) {
+ return;
+ }
+
+ if (document->IsResourceDoc()) {
+ return;
+ }
+
+ if (BrowsingContext* browsingContext = document->GetBrowsingContext()) {
+ if (browsingContext->GetEmbeddedInContentDocument()) {
+ return;
+ }
+ }
+
+ if (!document->IsTopLevelContentDocument()) {
+ return;
+ }
+ timing->NotifyLargestContentfulRenderForRootContentDocument(
+ GetReducedTimePrecisionDOMHighRes(mPerformance, mRenderTime));
+}
+} // namespace mozilla::dom
diff --git a/dom/performance/LargestContentfulPaint.h b/dom/performance/LargestContentfulPaint.h
new file mode 100644
index 0000000000..0193cd858a
--- /dev/null
+++ b/dom/performance/LargestContentfulPaint.h
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_LargestContentfulPaint_h___
+#define mozilla_dom_LargestContentfulPaint_h___
+
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/PerformanceEntry.h"
+#include "mozilla/dom/PerformanceLargestContentfulPaintBinding.h"
+
+#include "imgRequestProxy.h"
+
+class nsTextFrame;
+namespace mozilla::dom {
+
+static constexpr nsLiteralString kLargestContentfulPaintName =
+ u"largest-contentful-paint"_ns;
+
+class Performance;
+class PerformanceMainThread;
+
+struct LCPImageEntryKey {
+ LCPImageEntryKey(Element* aElement, imgRequestProxy* aImgRequestProxy)
+ : mElement(do_GetWeakReference(aElement)),
+ mImageRequestProxy(aImgRequestProxy) {
+ MOZ_ASSERT(aElement);
+ MOZ_ASSERT(aImgRequestProxy);
+
+ mHash = mozilla::HashGeneric(reinterpret_cast<uintptr_t>(aElement),
+ reinterpret_cast<uintptr_t>(aImgRequestProxy));
+ }
+
+ LCPImageEntryKey(const LCPImageEntryKey& aLCPImageEntryKey) {
+ mElement = aLCPImageEntryKey.mElement;
+ mImageRequestProxy = aLCPImageEntryKey.mImageRequestProxy;
+ mHash = aLCPImageEntryKey.mHash;
+ }
+
+ Element* GetElement() const {
+ nsCOMPtr<Element> element = do_QueryReferent(mElement);
+ return element;
+ }
+
+ imgRequestProxy* GetImgRequestProxy() const {
+ return static_cast<imgRequestProxy*>(mImageRequestProxy.get());
+ }
+
+ bool operator==(const LCPImageEntryKey& aOther) const {
+ imgRequestProxy* imgRequest = GetImgRequestProxy();
+ if (!imgRequest) {
+ return false;
+ }
+
+ imgRequestProxy* otherImgRequest = aOther.GetImgRequestProxy();
+ if (!otherImgRequest) {
+ return false;
+ }
+
+ Element* element = GetElement();
+ if (!element) {
+ return false;
+ }
+
+ Element* otherElement = aOther.GetElement();
+ if (!otherElement) {
+ return false;
+ }
+
+ return element == otherElement && imgRequest == otherImgRequest;
+ }
+
+ bool Equals(const Element* aElement,
+ const imgRequestProxy* aImgRequestProxy) const {
+ Element* element = GetElement();
+ if (!element || !mImageRequestProxy) {
+ return false;
+ }
+
+ return element == aElement && mImageRequestProxy == mImageRequestProxy;
+ }
+
+ nsWeakPtr mElement;
+
+ WeakPtr<PreloaderBase> mImageRequestProxy;
+
+ PLDHashNumber mHash = 0;
+
+ ~LCPImageEntryKey() = default;
+};
+
+struct LCPTextFrameHelper final {
+ static void MaybeUnionTextFrame(nsTextFrame* aTextFrame,
+ const nsRect& aRelativeToSelfRect);
+};
+
+class ImagePendingRendering final {
+ public:
+ ImagePendingRendering(const LCPImageEntryKey& aLCPImageEntryKey,
+ const TimeStamp& aLoadTime);
+
+ Element* GetElement() const { return mLCPImageEntryKey.GetElement(); }
+
+ imgRequestProxy* GetImgRequestProxy() const {
+ return mLCPImageEntryKey.GetImgRequestProxy();
+ }
+
+ LCPImageEntryKey mLCPImageEntryKey;
+ TimeStamp mLoadTime;
+};
+
+class LCPEntryHashEntry : public PLDHashEntryHdr {
+ public:
+ typedef const LCPImageEntryKey& KeyType;
+ typedef const LCPImageEntryKey* KeyTypePointer;
+
+ explicit LCPEntryHashEntry(KeyTypePointer aKey) : mKey(*aKey) {}
+ LCPEntryHashEntry(LCPEntryHashEntry&&) = default;
+
+ ~LCPEntryHashEntry() = default;
+
+ bool KeyEquals(KeyTypePointer aKey) const { return mKey == *aKey; }
+
+ KeyType GetKey() const { return mKey; }
+
+ static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
+
+ static PLDHashNumber HashKey(KeyTypePointer aKey) { return aKey->mHash; }
+ enum { ALLOW_MEMMOVE = true };
+
+ LCPImageEntryKey mKey;
+};
+
+class LCPHelpers final {
+ public:
+ // Creates the LCP Entry for images with all information except the size of
+ // the element. The size of the image is unknown at the moment. The entry is
+ // not going to be queued in this function.
+ static void CreateLCPEntryForImage(
+ PerformanceMainThread* aPerformance, Element* aElement,
+ imgRequestProxy* aRequestProxy, const TimeStamp& aLoadTime,
+ const TimeStamp& aRenderTime, const LCPImageEntryKey& aContentIdentifier);
+
+ // Called when the size of the image is known.
+ static void FinalizeLCPEntryForImage(Element* aContainingBlock,
+ imgRequestProxy* aImgRequestProxy,
+ const nsRect& aTargetRectRelativeToSelf);
+
+ static void FinalizeLCPEntryForText(PerformanceMainThread* aPerformance,
+ const TimeStamp& aRenderTime,
+ Element* aContainingBlock,
+ const nsRect& aTargetRectRelativeToSelf,
+ const nsPresContext* aPresContext);
+
+ static bool IsQualifiedImageRequest(imgRequest* aRequest,
+ Element* aContainingElement);
+
+ private:
+ static bool CanFinalizeLCPEntry(const nsIFrame* aFrame);
+};
+
+// https://w3c.github.io/largest-contentful-paint/
+class LargestContentfulPaint final : public PerformanceEntry {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(LargestContentfulPaint,
+ PerformanceEntry)
+
+ LargestContentfulPaint(PerformanceMainThread* aPerformance,
+ const TimeStamp& aRenderTime,
+ const Maybe<TimeStamp>& aLoadTime,
+ const unsigned long aSize, nsIURI* aURI,
+ Element* aElement,
+ const Maybe<const LCPImageEntryKey>& aLCPImageEntryKey,
+ bool aShouldExposeRenderTime);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ DOMHighResTimeStamp RenderTime() const;
+ DOMHighResTimeStamp LoadTime() const;
+ DOMHighResTimeStamp StartTime() const override;
+
+ unsigned long Size() const { return mSize; }
+ void GetId(nsAString& aId) const {
+ if (mId) {
+ mId->ToString(aId);
+ }
+ }
+ void GetUrl(nsAString& aUrl);
+
+ Element* GetElement() const;
+
+ static Element* GetContainingBlockForTextFrame(const nsTextFrame* aTextFrame);
+
+ void UpdateSize(const Element* aContainingBlock,
+ const nsRect& aTargetRectRelativeToSelf,
+ const PerformanceMainThread* aPerformance, bool aIsImage);
+
+ void BufferEntryIfNeeded() override;
+
+ static void MaybeProcessImageForElementTiming(imgRequestProxy* aRequest,
+ Element* aElement);
+
+ void QueueEntry();
+
+ Maybe<LCPImageEntryKey>& GetLCPImageEntryKey() { return mLCPImageEntryKey; }
+
+ private:
+ ~LargestContentfulPaint() = default;
+
+ void ReportLCPToNavigationTimings();
+
+ RefPtr<PerformanceMainThread> mPerformance;
+
+ // This is always set but only exposed to web content if
+ // mShouldExposeRenderTime is true.
+ const TimeStamp mRenderTime;
+ const Maybe<TimeStamp> mLoadTime;
+ // This is set to false when for security reasons web content it not allowed
+ // to see the RenderTime.
+ const bool mShouldExposeRenderTime;
+ unsigned long mSize;
+ nsCOMPtr<nsIURI> mURI;
+
+ nsWeakPtr mElement;
+ RefPtr<nsAtom> mId;
+
+ Maybe<LCPImageEntryKey> mLCPImageEntryKey;
+};
+} // namespace mozilla::dom
+#endif
diff --git a/dom/performance/Performance.cpp b/dom/performance/Performance.cpp
new file mode 100644
index 0000000000..ecbc3b4c68
--- /dev/null
+++ b/dom/performance/Performance.cpp
@@ -0,0 +1,1045 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "Performance.h"
+
+#include <sstream>
+
+#include "ETWTools.h"
+#include "GeckoProfiler.h"
+#include "nsRFPService.h"
+#include "PerformanceEntry.h"
+#include "PerformanceMainThread.h"
+#include "PerformanceMark.h"
+#include "PerformanceMeasure.h"
+#include "PerformanceObserver.h"
+#include "PerformanceResourceTiming.h"
+#include "PerformanceService.h"
+#include "PerformanceWorker.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/MessagePortBinding.h"
+#include "mozilla/dom/PerformanceBinding.h"
+#include "mozilla/dom/PerformanceEntryEvent.h"
+#include "mozilla/dom/PerformanceNavigationBinding.h"
+#include "mozilla/dom/PerformanceObserverBinding.h"
+#include "mozilla/dom/PerformanceNavigationTiming.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerScope.h"
+
+#define PERFLOG(msg, ...) printf_stderr(msg, ##__VA_ARGS__)
+
+namespace mozilla::dom {
+
+enum class Performance::ResolveTimestampAttribute {
+ Start,
+ End,
+ Duration,
+};
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Performance)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(Performance, DOMEventTargetHelper,
+ mUserEntries, mResourceEntries,
+ mSecondaryResourceEntries, mObservers);
+
+NS_IMPL_ADDREF_INHERITED(Performance, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(Performance, DOMEventTargetHelper)
+
+/* static */
+already_AddRefed<Performance> Performance::CreateForMainThread(
+ nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
+ nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(aWindow->AsGlobal());
+ RefPtr<Performance> performance =
+ new PerformanceMainThread(aWindow, aDOMTiming, aChannel);
+ return performance.forget();
+}
+
+/* static */
+already_AddRefed<Performance> Performance::CreateForWorker(
+ WorkerGlobalScope* aGlobalScope) {
+ MOZ_ASSERT(aGlobalScope);
+ // aWorkerPrivate->AssertIsOnWorkerThread();
+
+ RefPtr<Performance> performance = new PerformanceWorker(aGlobalScope);
+ return performance.forget();
+}
+
+/* static */
+already_AddRefed<Performance> Performance::Get(JSContext* aCx,
+ nsIGlobalObject* aGlobal) {
+ RefPtr<Performance> performance;
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal);
+ if (!window) {
+ return nullptr;
+ }
+
+ performance = window->GetPerformance();
+ return performance.forget();
+ }
+
+ const WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
+ if (!workerPrivate) {
+ return nullptr;
+ }
+
+ WorkerGlobalScope* scope = workerPrivate->GlobalScope();
+ MOZ_ASSERT(scope);
+ performance = scope->GetPerformance();
+
+ return performance.forget();
+}
+
+Performance::Performance(nsIGlobalObject* aGlobal)
+ : DOMEventTargetHelper(aGlobal),
+ mResourceTimingBufferSize(kDefaultResourceTimingBufferSize),
+ mPendingNotificationObserversTask(false),
+ mPendingResourceTimingBufferFullEvent(false),
+ mRTPCallerType(aGlobal->GetRTPCallerType()),
+ mCrossOriginIsolated(aGlobal->CrossOriginIsolated()),
+ mShouldResistFingerprinting(aGlobal->ShouldResistFingerprinting(
+ RFPTarget::ReduceTimerPrecision)) {}
+
+Performance::~Performance() = default;
+
+DOMHighResTimeStamp Performance::TimeStampToDOMHighResForRendering(
+ TimeStamp aTimeStamp) const {
+ DOMHighResTimeStamp stamp = GetDOMTiming()->TimeStampToDOMHighRes(aTimeStamp);
+ // 0 is an inappropriate mixin for this this area; however CSS Animations
+ // needs to have it's Time Reduction Logic refactored, so it's currently
+ // only clamping for RFP mode. RFP mode gives a much lower time precision,
+ // so we accept the security leak here for now.
+ return nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly(stamp, 0,
+ mRTPCallerType);
+}
+
+DOMHighResTimeStamp Performance::Now() {
+ DOMHighResTimeStamp rawTime = NowUnclamped();
+
+ // XXX: Removing this caused functions in pkcs11f.h to fail.
+ // Bug 1628021 investigates the root cause - it involves initializing
+ // the RNG service (part of GetRandomTimelineSeed()) off-main-thread
+ // but the underlying cause hasn't been identified yet.
+ if (mRTPCallerType == RTPCallerType::SystemPrincipal) {
+ return rawTime;
+ }
+
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawTime, GetRandomTimelineSeed(), mRTPCallerType);
+}
+
+DOMHighResTimeStamp Performance::NowUnclamped() const {
+ TimeDuration duration = TimeStamp::Now() - CreationTimeStamp();
+ return duration.ToMilliseconds();
+}
+
+DOMHighResTimeStamp Performance::TimeOrigin() {
+ if (!mPerformanceService) {
+ mPerformanceService = PerformanceService::GetOrCreate();
+ }
+
+ MOZ_ASSERT(mPerformanceService);
+ DOMHighResTimeStamp rawTimeOrigin =
+ mPerformanceService->TimeOrigin(CreationTimeStamp());
+ // Time Origin is an absolute timestamp, so we supply a 0 context mix-in
+ return nsRFPService::ReduceTimePrecisionAsMSecs(rawTimeOrigin, 0,
+ mRTPCallerType);
+}
+
+JSObject* Performance::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return Performance_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void Performance::GetEntries(nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ aRetval = mResourceEntries.Clone();
+ aRetval.AppendElements(mUserEntries);
+ aRetval.Sort(PerformanceEntryComparator());
+}
+
+void Performance::GetEntriesByType(
+ const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ if (aEntryType.EqualsLiteral("resource")) {
+ aRetval = mResourceEntries.Clone();
+ return;
+ }
+
+ aRetval.Clear();
+
+ if (aEntryType.EqualsLiteral("mark") || aEntryType.EqualsLiteral("measure")) {
+ RefPtr<nsAtom> entryType = NS_Atomize(aEntryType);
+ for (PerformanceEntry* entry : mUserEntries) {
+ if (entry->GetEntryType() == entryType) {
+ aRetval.AppendElement(entry);
+ }
+ }
+ }
+}
+
+void Performance::GetEntriesByName(
+ const nsAString& aName, const Optional<nsAString>& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ aRetval.Clear();
+
+ RefPtr<nsAtom> name = NS_Atomize(aName);
+ RefPtr<nsAtom> entryType =
+ aEntryType.WasPassed() ? NS_Atomize(aEntryType.Value()) : nullptr;
+
+ if (entryType) {
+ if (entryType == nsGkAtoms::mark || entryType == nsGkAtoms::measure) {
+ for (PerformanceEntry* entry : mUserEntries) {
+ if (entry->GetName() == name && entry->GetEntryType() == entryType) {
+ aRetval.AppendElement(entry);
+ }
+ }
+ return;
+ }
+ if (entryType == nsGkAtoms::resource) {
+ for (PerformanceEntry* entry : mResourceEntries) {
+ MOZ_ASSERT(entry->GetEntryType() == entryType);
+ if (entry->GetName() == name) {
+ aRetval.AppendElement(entry);
+ }
+ }
+ return;
+ }
+ // Invalid entryType
+ return;
+ }
+
+ nsTArray<PerformanceEntry*> qualifiedResourceEntries;
+ nsTArray<PerformanceEntry*> qualifiedUserEntries;
+ // ::Measure expects that results from this function are already
+ // passed through ReduceTimePrecision. mResourceEntries and mUserEntries
+ // are, so the invariant holds.
+ for (PerformanceEntry* entry : mResourceEntries) {
+ if (entry->GetName() == name) {
+ qualifiedResourceEntries.AppendElement(entry);
+ }
+ }
+
+ for (PerformanceEntry* entry : mUserEntries) {
+ if (entry->GetName() == name) {
+ qualifiedUserEntries.AppendElement(entry);
+ }
+ }
+
+ size_t resourceEntriesIdx = 0, userEntriesIdx = 0;
+ aRetval.SetCapacity(qualifiedResourceEntries.Length() +
+ qualifiedUserEntries.Length());
+
+ PerformanceEntryComparator comparator;
+
+ while (resourceEntriesIdx < qualifiedResourceEntries.Length() &&
+ userEntriesIdx < qualifiedUserEntries.Length()) {
+ if (comparator.LessThan(qualifiedResourceEntries[resourceEntriesIdx],
+ qualifiedUserEntries[userEntriesIdx])) {
+ aRetval.AppendElement(qualifiedResourceEntries[resourceEntriesIdx]);
+ ++resourceEntriesIdx;
+ } else {
+ aRetval.AppendElement(qualifiedUserEntries[userEntriesIdx]);
+ ++userEntriesIdx;
+ }
+ }
+
+ while (resourceEntriesIdx < qualifiedResourceEntries.Length()) {
+ aRetval.AppendElement(qualifiedResourceEntries[resourceEntriesIdx]);
+ ++resourceEntriesIdx;
+ }
+
+ while (userEntriesIdx < qualifiedUserEntries.Length()) {
+ aRetval.AppendElement(qualifiedUserEntries[userEntriesIdx]);
+ ++userEntriesIdx;
+ }
+}
+
+void Performance::GetEntriesByTypeForObserver(
+ const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ GetEntriesByType(aEntryType, aRetval);
+}
+
+void Performance::ClearUserEntries(const Optional<nsAString>& aEntryName,
+ const nsAString& aEntryType) {
+ MOZ_ASSERT(!aEntryType.IsEmpty());
+ RefPtr<nsAtom> name =
+ aEntryName.WasPassed() ? NS_Atomize(aEntryName.Value()) : nullptr;
+ RefPtr<nsAtom> entryType = NS_Atomize(aEntryType);
+ mUserEntries.RemoveElementsBy([name, entryType](const auto& entry) {
+ return (!name || entry->GetName() == name) &&
+ (entry->GetEntryType() == entryType);
+ });
+}
+
+void Performance::ClearResourceTimings() { mResourceEntries.Clear(); }
+
+struct UserTimingMarker : public BaseMarkerType<UserTimingMarker> {
+ static constexpr const char* Name = "UserTiming";
+ static constexpr const char* Description =
+ "UserTimingMeasure is created using the DOM API performance.measure().";
+
+ using MS = MarkerSchema;
+ static constexpr MS::PayloadField PayloadFields[] = {
+ {"name", MS::InputType::String, "User Marker Name", MS::Format::String,
+ MS::PayloadFlags::Searchable},
+ {"entryType", MS::InputType::Boolean, "Entry Type"},
+ {"startMark", MS::InputType::String, "Start Mark"},
+ {"endMark", MS::InputType::String, "End Mark"}};
+
+ static constexpr MS::Location Locations[] = {MS::Location::MarkerChart,
+ MS::Location::MarkerTable};
+ static constexpr const char* AllLabels = "{marker.data.name}";
+
+ static constexpr MS::ETWMarkerGroup Group = MS::ETWMarkerGroup::UserMarkers;
+
+ static void StreamJSONMarkerData(
+ baseprofiler::SpliceableJSONWriter& aWriter,
+ const ProfilerString16View& aName, bool aIsMeasure,
+ const Maybe<ProfilerString16View>& aStartMark,
+ const Maybe<ProfilerString16View>& aEndMark) {
+ StreamJSONMarkerDataImpl(
+ aWriter, aName,
+ aIsMeasure ? MakeStringSpan("measure") : MakeStringSpan("mark"),
+ aStartMark, aEndMark);
+ }
+};
+
+already_AddRefed<PerformanceMark> Performance::Mark(
+ JSContext* aCx, const nsAString& aName,
+ const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> parent = GetParentObject();
+ if (!parent || parent->IsDying() || !parent->HasJSGlobal()) {
+ aRv.ThrowInvalidStateError("Global object is unavailable");
+ return nullptr;
+ }
+
+ GlobalObject global(aCx, parent->GetGlobalJSObject());
+ if (global.Failed()) {
+ aRv.ThrowInvalidStateError("Global object is unavailable");
+ return nullptr;
+ }
+
+ RefPtr<PerformanceMark> performanceMark =
+ PerformanceMark::Constructor(global, aName, aMarkOptions, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ InsertUserEntry(performanceMark);
+
+ if (profiler_is_collecting_markers()) {
+ Maybe<uint64_t> innerWindowId;
+ if (GetOwner()) {
+ innerWindowId = Some(GetOwner()->WindowID());
+ }
+ TimeStamp startTimeStamp =
+ CreationTimeStamp() +
+ TimeDuration::FromMilliseconds(performanceMark->UnclampedStartTime());
+ profiler_add_marker("UserTiming", geckoprofiler::category::DOM,
+ MarkerOptions(MarkerTiming::InstantAt(startTimeStamp),
+ MarkerInnerWindowId(innerWindowId)),
+ UserTimingMarker{}, aName, /* aIsMeasure */ false,
+ Nothing{}, Nothing{});
+ }
+
+ return performanceMark.forget();
+}
+
+void Performance::ClearMarks(const Optional<nsAString>& aName) {
+ ClearUserEntries(aName, u"mark"_ns);
+}
+
+// To be removed once bug 1124165 lands
+bool Performance::IsPerformanceTimingAttribute(const nsAString& aName) const {
+ // Note that toJSON is added to this list due to bug 1047848
+ static const char* attributes[] = {"navigationStart",
+ "unloadEventStart",
+ "unloadEventEnd",
+ "redirectStart",
+ "redirectEnd",
+ "fetchStart",
+ "domainLookupStart",
+ "domainLookupEnd",
+ "connectStart",
+ "secureConnectionStart",
+ "connectEnd",
+ "requestStart",
+ "responseStart",
+ "responseEnd",
+ "domLoading",
+ "domInteractive",
+ "domContentLoadedEventStart",
+ "domContentLoadedEventEnd",
+ "domComplete",
+ "loadEventStart",
+ "loadEventEnd",
+ nullptr};
+
+ for (uint32_t i = 0; attributes[i]; ++i) {
+ if (aName.EqualsASCII(attributes[i])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithString(
+ const nsAString& aName, ErrorResult& aRv, bool aReturnUnclamped) {
+ if (IsPerformanceTimingAttribute(aName)) {
+ return ConvertNameToTimestamp(aName, aRv);
+ }
+
+ RefPtr<nsAtom> name = NS_Atomize(aName);
+ // Just loop over the user entries
+ for (const PerformanceEntry* entry : Reversed(mUserEntries)) {
+ if (entry->GetName() == name && entry->GetEntryType() == nsGkAtoms::mark) {
+ if (aReturnUnclamped) {
+ return entry->UnclampedStartTime();
+ }
+ return entry->StartTime();
+ }
+ }
+
+ nsPrintfCString errorMsg("Given mark name, %s, is unknown",
+ NS_ConvertUTF16toUTF8(aName).get());
+ aRv.ThrowSyntaxError(errorMsg);
+ return 0;
+}
+
+DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithDOMHighResTimeStamp(
+ const ResolveTimestampAttribute aAttribute,
+ const DOMHighResTimeStamp aTimestamp, ErrorResult& aRv) {
+ if (aTimestamp < 0) {
+ nsAutoCString attributeName;
+ switch (aAttribute) {
+ case ResolveTimestampAttribute::Start:
+ attributeName = "start";
+ break;
+ case ResolveTimestampAttribute::End:
+ attributeName = "end";
+ break;
+ case ResolveTimestampAttribute::Duration:
+ attributeName = "duration";
+ break;
+ }
+
+ nsPrintfCString errorMsg("Given attribute %s cannot be negative",
+ attributeName.get());
+ aRv.ThrowTypeError(errorMsg);
+ }
+ return aTimestamp;
+}
+
+DOMHighResTimeStamp Performance::ConvertMarkToTimestamp(
+ const ResolveTimestampAttribute aAttribute,
+ const OwningStringOrDouble& aMarkNameOrTimestamp, ErrorResult& aRv,
+ bool aReturnUnclamped) {
+ if (aMarkNameOrTimestamp.IsString()) {
+ return ConvertMarkToTimestampWithString(aMarkNameOrTimestamp.GetAsString(),
+ aRv, aReturnUnclamped);
+ }
+
+ return ConvertMarkToTimestampWithDOMHighResTimeStamp(
+ aAttribute, aMarkNameOrTimestamp.GetAsDouble(), aRv);
+}
+
+DOMHighResTimeStamp Performance::ConvertNameToTimestamp(const nsAString& aName,
+ ErrorResult& aRv) {
+ if (!IsGlobalObjectWindow()) {
+ nsPrintfCString errorMsg(
+ "Cannot get PerformanceTiming attribute values for non-Window global "
+ "object. Given: %s",
+ NS_ConvertUTF16toUTF8(aName).get());
+ aRv.ThrowTypeError(errorMsg);
+ return 0;
+ }
+
+ if (aName.EqualsASCII("navigationStart")) {
+ return 0;
+ }
+
+ // We use GetPerformanceTimingFromString, rather than calling the
+ // navigationStart method timing function directly, because the former handles
+ // reducing precision against timing attacks.
+ const DOMHighResTimeStamp startTime =
+ GetPerformanceTimingFromString(u"navigationStart"_ns);
+ const DOMHighResTimeStamp endTime = GetPerformanceTimingFromString(aName);
+ MOZ_ASSERT(endTime >= 0);
+ if (endTime == 0) {
+ nsPrintfCString errorMsg(
+ "Given PerformanceTiming attribute, %s, isn't available yet",
+ NS_ConvertUTF16toUTF8(aName).get());
+ aRv.ThrowInvalidAccessError(errorMsg);
+ return 0;
+ }
+
+ return endTime - startTime;
+}
+
+DOMHighResTimeStamp Performance::ResolveEndTimeForMeasure(
+ const Optional<nsAString>& aEndMark,
+ const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv,
+ bool aReturnUnclamped) {
+ DOMHighResTimeStamp endTime;
+ if (aEndMark.WasPassed()) {
+ endTime = ConvertMarkToTimestampWithString(aEndMark.Value(), aRv,
+ aReturnUnclamped);
+ } else if (aOptions && aOptions->mEnd.WasPassed()) {
+ endTime =
+ ConvertMarkToTimestamp(ResolveTimestampAttribute::End,
+ aOptions->mEnd.Value(), aRv, aReturnUnclamped);
+ } else if (aOptions && aOptions->mStart.WasPassed() &&
+ aOptions->mDuration.WasPassed()) {
+ const DOMHighResTimeStamp start =
+ ConvertMarkToTimestamp(ResolveTimestampAttribute::Start,
+ aOptions->mStart.Value(), aRv, aReturnUnclamped);
+ if (aRv.Failed()) {
+ return 0;
+ }
+
+ const DOMHighResTimeStamp duration =
+ ConvertMarkToTimestampWithDOMHighResTimeStamp(
+ ResolveTimestampAttribute::Duration, aOptions->mDuration.Value(),
+ aRv);
+ if (aRv.Failed()) {
+ return 0;
+ }
+
+ endTime = start + duration;
+ } else {
+ endTime = Now();
+ }
+
+ return endTime;
+}
+
+DOMHighResTimeStamp Performance::ResolveStartTimeForMeasure(
+ const Maybe<const nsAString&>& aStartMark,
+ const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv,
+ bool aReturnUnclamped) {
+ DOMHighResTimeStamp startTime;
+ if (aOptions && aOptions->mStart.WasPassed()) {
+ startTime =
+ ConvertMarkToTimestamp(ResolveTimestampAttribute::Start,
+ aOptions->mStart.Value(), aRv, aReturnUnclamped);
+ } else if (aOptions && aOptions->mDuration.WasPassed() &&
+ aOptions->mEnd.WasPassed()) {
+ const DOMHighResTimeStamp duration =
+ ConvertMarkToTimestampWithDOMHighResTimeStamp(
+ ResolveTimestampAttribute::Duration, aOptions->mDuration.Value(),
+ aRv);
+ if (aRv.Failed()) {
+ return 0;
+ }
+
+ const DOMHighResTimeStamp end =
+ ConvertMarkToTimestamp(ResolveTimestampAttribute::End,
+ aOptions->mEnd.Value(), aRv, aReturnUnclamped);
+ if (aRv.Failed()) {
+ return 0;
+ }
+
+ startTime = end - duration;
+ } else if (aStartMark) {
+ startTime =
+ ConvertMarkToTimestampWithString(*aStartMark, aRv, aReturnUnclamped);
+ } else {
+ startTime = 0;
+ }
+
+ return startTime;
+}
+
+static std::string GetMarkerFilename() {
+ std::stringstream s;
+ if (char* markerDir = getenv("MOZ_PERFORMANCE_MARKER_DIR")) {
+ s << markerDir << "/";
+ }
+#ifdef XP_WIN
+ s << "marker-" << GetCurrentProcessId() << ".txt";
+#else
+ s << "marker-" << getpid() << ".txt";
+#endif
+ return s.str();
+}
+
+std::pair<TimeStamp, TimeStamp> Performance::GetTimeStampsForMarker(
+ const Maybe<const nsAString&>& aStartMark,
+ const Optional<nsAString>& aEndMark,
+ const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv) {
+ const DOMHighResTimeStamp unclampedStartTime = ResolveStartTimeForMeasure(
+ aStartMark, aOptions, aRv, /* aReturnUnclamped */ true);
+ const DOMHighResTimeStamp unclampedEndTime =
+ ResolveEndTimeForMeasure(aEndMark, aOptions, aRv, /* aReturnUnclamped */
+ true);
+
+ TimeStamp startTimeStamp =
+ CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedStartTime);
+ TimeStamp endTimeStamp =
+ CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedEndTime);
+
+ return std::make_pair(startTimeStamp, endTimeStamp);
+}
+
+// This emits markers to an external marker-[pid].txt file for use by an
+// external profiler like samply or etw-gecko
+void Performance::MaybeEmitExternalProfilerMarker(
+ const nsAString& aName, Maybe<const PerformanceMeasureOptions&> aOptions,
+ Maybe<const nsAString&> aStartMark, const Optional<nsAString>& aEndMark) {
+ static FILE* markerFile = getenv("MOZ_USE_PERFORMANCE_MARKER_FILE")
+ ? fopen(GetMarkerFilename().c_str(), "w+")
+ : nullptr;
+ if (!markerFile) {
+ return;
+ }
+
+ ErrorResult rv;
+ auto [startTimeStamp, endTimeStamp] =
+ GetTimeStampsForMarker(aStartMark, aEndMark, aOptions, rv);
+
+ if (NS_WARN_IF(rv.Failed())) {
+ return;
+ }
+
+#ifdef XP_LINUX
+ uint64_t rawStart = startTimeStamp.RawClockMonotonicNanosecondsSinceBoot();
+ uint64_t rawEnd = endTimeStamp.RawClockMonotonicNanosecondsSinceBoot();
+#elif XP_WIN
+ uint64_t rawStart = startTimeStamp.RawQueryPerformanceCounterValue().value();
+ uint64_t rawEnd = endTimeStamp.RawQueryPerformanceCounterValue().value();
+#elif XP_MACOSX
+ uint64_t rawStart = startTimeStamp.RawMachAbsoluteTimeNanoseconds();
+ uint64_t rawEnd = endTimeStamp.RawMachAbsoluteTimeNanoseconds();
+#else
+ uint64_t rawStart = 0;
+ uint64_t rawEnd = 0;
+ MOZ_CRASH("no timestamp");
+#endif
+ // Write a line for this measure to the marker file. The marker file uses a
+ // text-based format where every line is one marker, and each line has the
+ // format:
+ // `<raw_start_timestamp> <raw_end_timestamp> <measure_name>`
+ //
+ // The timestamp value is OS specific.
+ fprintf(markerFile, "%" PRIu64 " %" PRIu64 " %s\n", rawStart, rawEnd,
+ NS_ConvertUTF16toUTF8(aName).get());
+ fflush(markerFile);
+}
+
+already_AddRefed<PerformanceMeasure> Performance::Measure(
+ JSContext* aCx, const nsAString& aName,
+ const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions,
+ const Optional<nsAString>& aEndMark, ErrorResult& aRv) {
+ if (!GetParentObject()) {
+ aRv.ThrowInvalidStateError("Global object is unavailable");
+ return nullptr;
+ }
+
+ // Maybe is more readable than using the union type directly.
+ Maybe<const PerformanceMeasureOptions&> options;
+ if (aStartOrMeasureOptions.IsPerformanceMeasureOptions()) {
+ options.emplace(aStartOrMeasureOptions.GetAsPerformanceMeasureOptions());
+ }
+
+ const bool isOptionsNotEmpty =
+ options.isSome() &&
+ (!options->mDetail.isUndefined() || options->mStart.WasPassed() ||
+ options->mEnd.WasPassed() || options->mDuration.WasPassed());
+ if (isOptionsNotEmpty) {
+ if (aEndMark.WasPassed()) {
+ aRv.ThrowTypeError(
+ "Cannot provide separate endMark argument if "
+ "PerformanceMeasureOptions argument is given");
+ return nullptr;
+ }
+
+ if (!options->mStart.WasPassed() && !options->mEnd.WasPassed()) {
+ aRv.ThrowTypeError(
+ "PerformanceMeasureOptions must have start and/or end member");
+ return nullptr;
+ }
+
+ if (options->mStart.WasPassed() && options->mDuration.WasPassed() &&
+ options->mEnd.WasPassed()) {
+ aRv.ThrowTypeError(
+ "PerformanceMeasureOptions cannot have all of the following members: "
+ "start, duration, and end");
+ return nullptr;
+ }
+ }
+
+ const DOMHighResTimeStamp endTime = ResolveEndTimeForMeasure(
+ aEndMark, options, aRv, /* aReturnUnclamped */ false);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // Convert to Maybe for consistency with options.
+ Maybe<const nsAString&> startMark;
+ if (aStartOrMeasureOptions.IsString()) {
+ startMark.emplace(aStartOrMeasureOptions.GetAsString());
+ }
+ const DOMHighResTimeStamp startTime = ResolveStartTimeForMeasure(
+ startMark, options, aRv, /* aReturnUnclamped */ false);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ JS::Rooted<JS::Value> detail(aCx);
+ if (options && !options->mDetail.isNullOrUndefined()) {
+ StructuredSerializeOptions serializeOptions;
+ JS::Rooted<JS::Value> valueToClone(aCx, options->mDetail);
+ nsContentUtils::StructuredClone(aCx, GetParentObject(), valueToClone,
+ serializeOptions, &detail, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ } else {
+ detail.setNull();
+ }
+
+ RefPtr<PerformanceMeasure> performanceMeasure = new PerformanceMeasure(
+ GetParentObject(), aName, startTime, endTime, detail);
+ InsertUserEntry(performanceMeasure);
+
+ MaybeEmitExternalProfilerMarker(aName, options, startMark, aEndMark);
+
+ if (profiler_is_collecting_markers()) {
+ auto [startTimeStamp, endTimeStamp] =
+ GetTimeStampsForMarker(startMark, aEndMark, options, aRv);
+
+ Maybe<nsString> endMark;
+ if (aEndMark.WasPassed()) {
+ endMark.emplace(aEndMark.Value());
+ }
+
+ Maybe<uint64_t> innerWindowId;
+ if (GetOwner()) {
+ innerWindowId = Some(GetOwner()->WindowID());
+ }
+ profiler_add_marker("UserTiming", geckoprofiler::category::DOM,
+ {MarkerTiming::Interval(startTimeStamp, endTimeStamp),
+ MarkerInnerWindowId(innerWindowId)},
+ UserTimingMarker{}, aName, /* aIsMeasure */ true,
+ startMark, endMark);
+ }
+
+ return performanceMeasure.forget();
+}
+
+void Performance::ClearMeasures(const Optional<nsAString>& aName) {
+ ClearUserEntries(aName, u"measure"_ns);
+}
+
+void Performance::LogEntry(PerformanceEntry* aEntry,
+ const nsACString& aOwner) const {
+ PERFLOG("Performance Entry: %s|%s|%s|%f|%f|%" PRIu64 "\n",
+ aOwner.BeginReading(),
+ NS_ConvertUTF16toUTF8(aEntry->GetEntryType()->GetUTF16String()).get(),
+ NS_ConvertUTF16toUTF8(aEntry->GetName()->GetUTF16String()).get(),
+ aEntry->StartTime(), aEntry->Duration(),
+ static_cast<uint64_t>(PR_Now() / PR_USEC_PER_MSEC));
+}
+
+void Performance::TimingNotification(PerformanceEntry* aEntry,
+ const nsACString& aOwner,
+ const double aEpoch) {
+ PerformanceEntryEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ aEntry->GetName(init.mName);
+ aEntry->GetEntryType(init.mEntryType);
+ init.mStartTime = aEntry->StartTime();
+ init.mDuration = aEntry->Duration();
+ init.mEpoch = aEpoch;
+ CopyUTF8toUTF16(aOwner, init.mOrigin);
+
+ RefPtr<PerformanceEntryEvent> perfEntryEvent =
+ PerformanceEntryEvent::Constructor(this, u"performanceentry"_ns, init);
+
+ nsCOMPtr<EventTarget> et = do_QueryInterface(GetOwner());
+ if (et) {
+ et->DispatchEvent(*perfEntryEvent);
+ }
+}
+
+void Performance::InsertUserEntry(PerformanceEntry* aEntry) {
+ mUserEntries.InsertElementSorted(aEntry, PerformanceEntryComparator());
+
+ QueueEntry(aEntry);
+}
+
+/*
+ * Steps are labeled according to the description found at
+ * https://w3c.github.io/resource-timing/#sec-extensions-performance-interface.
+ *
+ * Buffer Full Event
+ */
+void Performance::BufferEvent() {
+ /*
+ * While resource timing secondary buffer is not empty,
+ * run the following substeps:
+ */
+ while (!mSecondaryResourceEntries.IsEmpty()) {
+ uint32_t secondaryResourceEntriesBeforeCount = 0;
+ uint32_t secondaryResourceEntriesAfterCount = 0;
+
+ /*
+ * Let number of excess entries before be resource
+ * timing secondary buffer current size.
+ */
+ secondaryResourceEntriesBeforeCount = mSecondaryResourceEntries.Length();
+
+ /*
+ * If can add resource timing entry returns false,
+ * then fire an event named resourcetimingbufferfull
+ * at the Performance object.
+ */
+ if (!CanAddResourceTimingEntry()) {
+ DispatchBufferFullEvent();
+ }
+
+ /*
+ * Run copy secondary buffer.
+ *
+ * While resource timing secondary buffer is not
+ * empty and can add resource timing entry returns
+ * true ...
+ */
+ while (!mSecondaryResourceEntries.IsEmpty() &&
+ CanAddResourceTimingEntry()) {
+ /*
+ * Let entry be the oldest PerformanceResourceTiming
+ * in resource timing secondary buffer. Add entry to
+ * the end of performance entry buffer. Increment
+ * resource timing buffer current size by 1.
+ */
+ mResourceEntries.InsertElementSorted(
+ mSecondaryResourceEntries.ElementAt(0), PerformanceEntryComparator());
+ /*
+ * Remove entry from resource timing secondary buffer.
+ * Decrement resource timing secondary buffer current
+ * size by 1.
+ */
+ mSecondaryResourceEntries.RemoveElementAt(0);
+ }
+
+ /*
+ * Let number of excess entries after be resource
+ * timing secondary buffer current size.
+ */
+ secondaryResourceEntriesAfterCount = mSecondaryResourceEntries.Length();
+
+ /*
+ * If number of excess entries before is lower than
+ * or equals number of excess entries after, then
+ * remove all entries from resource timing secondary
+ * buffer, set resource timing secondary buffer current
+ * size to 0, and abort these steps.
+ */
+ if (secondaryResourceEntriesBeforeCount <=
+ secondaryResourceEntriesAfterCount) {
+ mSecondaryResourceEntries.Clear();
+ break;
+ }
+ }
+ /*
+ * Set resource timing buffer full event pending flag
+ * to false.
+ */
+ mPendingResourceTimingBufferFullEvent = false;
+}
+
+void Performance::SetResourceTimingBufferSize(uint64_t aMaxSize) {
+ mResourceTimingBufferSize = aMaxSize;
+}
+
+/*
+ * Steps are labeled according to the description found at
+ * https://w3c.github.io/resource-timing/#sec-extensions-performance-interface.
+ *
+ * Can Add Resource Timing Entry
+ */
+MOZ_ALWAYS_INLINE bool Performance::CanAddResourceTimingEntry() {
+ /*
+ * If resource timing buffer current size is smaller than resource timing
+ * buffer size limit, return true. [Otherwise,] [r]eturn false.
+ */
+ return mResourceEntries.Length() < mResourceTimingBufferSize;
+}
+
+/*
+ * Steps are labeled according to the description found at
+ * https://w3c.github.io/resource-timing/#sec-extensions-performance-interface.
+ *
+ * Add a PerformanceResourceTiming Entry
+ */
+void Performance::InsertResourceEntry(PerformanceEntry* aEntry) {
+ MOZ_ASSERT(aEntry);
+
+ QueueEntry(aEntry);
+
+ /*
+ * Let new entry be the input PerformanceEntry to be added.
+ *
+ * If can add resource timing entry returns true and resource
+ * timing buffer full event pending flag is false ...
+ */
+ if (CanAddResourceTimingEntry() && !mPendingResourceTimingBufferFullEvent) {
+ /*
+ * Add new entry to the performance entry buffer.
+ * Increase resource timing buffer current size by 1.
+ */
+ mResourceEntries.InsertElementSorted(aEntry, PerformanceEntryComparator());
+ return;
+ }
+
+ /*
+ * If resource timing buffer full event pending flag is
+ * false ...
+ */
+ if (!mPendingResourceTimingBufferFullEvent) {
+ /*
+ * Set resource timing buffer full event pending flag
+ * to true.
+ */
+ mPendingResourceTimingBufferFullEvent = true;
+
+ /*
+ * Queue a task to run fire a buffer full event.
+ */
+ NS_DispatchToCurrentThread(NewCancelableRunnableMethod(
+ "Performance::BufferEvent", this, &Performance::BufferEvent));
+ }
+ /*
+ * Add new entry to the resource timing secondary buffer.
+ * Increase resource timing secondary buffer current size
+ * by 1.
+ */
+ mSecondaryResourceEntries.InsertElementSorted(aEntry,
+ PerformanceEntryComparator());
+}
+
+void Performance::AddObserver(PerformanceObserver* aObserver) {
+ mObservers.AppendElementUnlessExists(aObserver);
+}
+
+void Performance::RemoveObserver(PerformanceObserver* aObserver) {
+ mObservers.RemoveElement(aObserver);
+}
+
+void Performance::NotifyObservers() {
+ mPendingNotificationObserversTask = false;
+ NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(mObservers, Notify, ());
+}
+
+void Performance::CancelNotificationObservers() {
+ mPendingNotificationObserversTask = false;
+}
+
+class NotifyObserversTask final : public CancelableRunnable {
+ public:
+ explicit NotifyObserversTask(Performance* aPerformance)
+ : CancelableRunnable("dom::NotifyObserversTask"),
+ mPerformance(aPerformance) {
+ MOZ_ASSERT(mPerformance);
+ }
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY for now until Runnable::Run is
+ // MOZ_CAN_RUN_SCRIPT.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(mPerformance);
+ RefPtr<Performance> performance(mPerformance);
+ performance->NotifyObservers();
+ return NS_OK;
+ }
+
+ nsresult Cancel() override {
+ mPerformance->CancelNotificationObservers();
+ mPerformance = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ ~NotifyObserversTask() = default;
+
+ RefPtr<Performance> mPerformance;
+};
+
+void Performance::QueueNotificationObserversTask() {
+ if (!mPendingNotificationObserversTask) {
+ RunNotificationObserversTask();
+ }
+}
+
+void Performance::RunNotificationObserversTask() {
+ mPendingNotificationObserversTask = true;
+ nsCOMPtr<nsIRunnable> task = new NotifyObserversTask(this);
+ nsresult rv;
+ if (nsIGlobalObject* global = GetOwnerGlobal()) {
+ rv = global->Dispatch(task.forget());
+ } else {
+ rv = NS_DispatchToCurrentThread(task.forget());
+ }
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mPendingNotificationObserversTask = false;
+ }
+}
+
+void Performance::QueueEntry(PerformanceEntry* aEntry) {
+ nsTObserverArray<PerformanceObserver*> interestedObservers;
+ if (!mObservers.IsEmpty()) {
+ const auto [begin, end] = mObservers.NonObservingRange();
+ std::copy_if(begin, end, MakeBackInserter(interestedObservers),
+ [aEntry](PerformanceObserver* observer) {
+ return observer->ObservesTypeOfEntry(aEntry);
+ });
+ }
+
+ NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(interestedObservers, QueueEntry,
+ (aEntry));
+
+ aEntry->BufferEntryIfNeeded();
+
+ if (!interestedObservers.IsEmpty()) {
+ QueueNotificationObserversTask();
+ }
+}
+
+// We could clear User entries here, but doing so could break sites that call
+// performance.measure() if the marks disappeared without warning. Chrome
+// allows "infinite" entries.
+void Performance::MemoryPressure() {}
+
+size_t Performance::SizeOfUserEntries(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t userEntries = 0;
+ for (const PerformanceEntry* entry : mUserEntries) {
+ userEntries += entry->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return userEntries;
+}
+
+size_t Performance::SizeOfResourceEntries(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t resourceEntries = 0;
+ for (const PerformanceEntry* entry : mResourceEntries) {
+ resourceEntries += entry->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return resourceEntries;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/performance/Performance.h b/dom/performance/Performance.h
new file mode 100644
index 0000000000..982b5c04ec
--- /dev/null
+++ b/dom/performance/Performance.h
@@ -0,0 +1,259 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_Performance_h
+#define mozilla_dom_Performance_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsTObserverArray.h"
+
+class nsITimedChannel;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class OwningStringOrDouble;
+class StringOrPerformanceMeasureOptions;
+class PerformanceEntry;
+class PerformanceMark;
+struct PerformanceMarkOptions;
+struct PerformanceMeasureOptions;
+class PerformanceMeasure;
+class PerformanceNavigation;
+class PerformancePaintTiming;
+class PerformanceObserver;
+class PerformanceService;
+class PerformanceStorage;
+class PerformanceTiming;
+class PerformanceEventTiming;
+class WorkerGlobalScope;
+class EventCounts;
+
+// Base class for main-thread and worker Performance API
+class Performance : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Performance, DOMEventTargetHelper)
+
+ static bool IsObserverEnabled(JSContext* aCx, JSObject* aGlobal);
+
+ static already_AddRefed<Performance> CreateForMainThread(
+ nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
+ nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel);
+
+ static already_AddRefed<Performance> CreateForWorker(
+ WorkerGlobalScope* aGlobalScope);
+
+ // This will return nullptr if called outside of a Window or Worker.
+ static already_AddRefed<Performance> Get(JSContext* aCx,
+ nsIGlobalObject* aGlobal);
+
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual void GetEntries(nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+
+ virtual void GetEntriesByType(const nsAString& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+
+ virtual void GetEntriesByTypeForObserver(
+ const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+
+ virtual void GetEntriesByName(const nsAString& aName,
+ const Optional<nsAString>& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+
+ virtual PerformanceStorage* AsPerformanceStorage() = 0;
+
+ void ClearResourceTimings();
+
+ DOMHighResTimeStamp Now();
+
+ DOMHighResTimeStamp NowUnclamped() const;
+
+ DOMHighResTimeStamp TimeOrigin();
+
+ already_AddRefed<PerformanceMark> Mark(
+ JSContext* aCx, const nsAString& aName,
+ const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv);
+
+ void ClearMarks(const Optional<nsAString>& aName);
+
+ already_AddRefed<PerformanceMeasure> Measure(
+ JSContext* aCx, const nsAString& aName,
+ const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions,
+ const Optional<nsAString>& aEndMark, ErrorResult& aRv);
+
+ void ClearMeasures(const Optional<nsAString>& aName);
+
+ void SetResourceTimingBufferSize(uint64_t aMaxSize);
+
+ void AddObserver(PerformanceObserver* aObserver);
+ void RemoveObserver(PerformanceObserver* aObserver);
+ MOZ_CAN_RUN_SCRIPT void NotifyObservers();
+ void CancelNotificationObservers();
+
+ virtual PerformanceTiming* Timing() = 0;
+
+ virtual PerformanceNavigation* Navigation() = 0;
+
+ virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) = 0;
+
+ IMPL_EVENT_HANDLER(resourcetimingbufferfull)
+
+ virtual void GetMozMemory(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aObj) = 0;
+
+ virtual nsDOMNavigationTiming* GetDOMTiming() const = 0;
+
+ virtual nsITimedChannel* GetChannel() const = 0;
+
+ virtual TimeStamp CreationTimeStamp() const = 0;
+
+ RTPCallerType GetRTPCallerType() const { return mRTPCallerType; }
+
+ bool CrossOriginIsolated() const { return mCrossOriginIsolated; }
+ bool ShouldResistFingerprinting() const {
+ return mShouldResistFingerprinting;
+ }
+
+ DOMHighResTimeStamp TimeStampToDOMHighResForRendering(TimeStamp) const;
+
+ virtual uint64_t GetRandomTimelineSeed() = 0;
+
+ void MemoryPressure();
+
+ size_t SizeOfUserEntries(mozilla::MallocSizeOf aMallocSizeOf) const;
+ size_t SizeOfResourceEntries(mozilla::MallocSizeOf aMallocSizeOf) const;
+ virtual size_t SizeOfEventEntries(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return 0;
+ }
+
+ void InsertResourceEntry(PerformanceEntry* aEntry);
+
+ virtual void InsertEventTimingEntry(PerformanceEventTiming* aEntry) = 0;
+
+ virtual void BufferEventTimingEntryIfNeeded(
+ PerformanceEventTiming* aEntry) = 0;
+
+ virtual class EventCounts* EventCounts() = 0;
+
+ virtual void QueueNavigationTimingEntry() = 0;
+
+ virtual void UpdateNavigationTimingEntry() = 0;
+
+ virtual void DispatchPendingEventTimingEntries() = 0;
+
+ void QueueNotificationObserversTask();
+
+ bool IsPerformanceTimingAttribute(const nsAString& aName) const;
+
+ virtual bool IsGlobalObjectWindow() const { return false; };
+
+ protected:
+ Performance(nsIGlobalObject* aGlobal);
+ Performance(nsPIDOMWindowInner* aWindow);
+
+ virtual ~Performance();
+
+ virtual void InsertUserEntry(PerformanceEntry* aEntry);
+
+ void ClearUserEntries(const Optional<nsAString>& aEntryName,
+ const nsAString& aEntryType);
+
+ virtual void DispatchBufferFullEvent() = 0;
+
+ virtual DOMHighResTimeStamp CreationTime() const = 0;
+
+ virtual DOMHighResTimeStamp GetPerformanceTimingFromString(
+ const nsAString& aTimingName) {
+ return 0;
+ }
+
+ void LogEntry(PerformanceEntry* aEntry, const nsACString& aOwner) const;
+ void TimingNotification(PerformanceEntry* aEntry, const nsACString& aOwner,
+ const double aEpoch);
+
+ void RunNotificationObserversTask();
+ void QueueEntry(PerformanceEntry* aEntry);
+
+ nsTObserverArray<RefPtr<PerformanceObserver>> mObservers;
+
+ protected:
+ static const uint64_t kDefaultResourceTimingBufferSize = 250;
+
+ // When kDefaultResourceTimingBufferSize is increased or removed, these should
+ // be changed to use SegmentedVector
+ AutoTArray<RefPtr<PerformanceEntry>, kDefaultResourceTimingBufferSize>
+ mUserEntries;
+ AutoTArray<RefPtr<PerformanceEntry>, kDefaultResourceTimingBufferSize>
+ mResourceEntries;
+ AutoTArray<RefPtr<PerformanceEntry>, kDefaultResourceTimingBufferSize>
+ mSecondaryResourceEntries;
+
+ uint64_t mResourceTimingBufferSize;
+ bool mPendingNotificationObserversTask;
+
+ bool mPendingResourceTimingBufferFullEvent;
+
+ RefPtr<PerformanceService> mPerformanceService;
+
+ const RTPCallerType mRTPCallerType;
+ const bool mCrossOriginIsolated;
+ const bool mShouldResistFingerprinting;
+
+ private:
+ MOZ_ALWAYS_INLINE bool CanAddResourceTimingEntry();
+ void BufferEvent();
+ void MaybeEmitExternalProfilerMarker(
+ const nsAString& aName, Maybe<const PerformanceMeasureOptions&> aOptions,
+ Maybe<const nsAString&> aStartMark, const Optional<nsAString>& aEndMark);
+
+ // The attributes of a PerformanceMeasureOptions that we call
+ // ResolveTimestamp* on.
+ enum class ResolveTimestampAttribute;
+
+ DOMHighResTimeStamp ConvertMarkToTimestampWithString(const nsAString& aName,
+ ErrorResult& aRv,
+ bool aReturnUnclamped);
+ DOMHighResTimeStamp ConvertMarkToTimestampWithDOMHighResTimeStamp(
+ const ResolveTimestampAttribute aAttribute, const double aTimestamp,
+ ErrorResult& aRv);
+ DOMHighResTimeStamp ConvertMarkToTimestamp(
+ const ResolveTimestampAttribute aAttribute,
+ const OwningStringOrDouble& aMarkNameOrTimestamp, ErrorResult& aRv,
+ bool aReturnUnclamped);
+
+ DOMHighResTimeStamp ConvertNameToTimestamp(const nsAString& aName,
+ ErrorResult& aRv);
+
+ DOMHighResTimeStamp ResolveEndTimeForMeasure(
+ const Optional<nsAString>& aEndMark,
+ const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv,
+ bool aReturnUnclamped);
+ DOMHighResTimeStamp ResolveStartTimeForMeasure(
+ const Maybe<const nsAString&>& aStartMark,
+ const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv,
+ bool aReturnUnclamped);
+
+ std::pair<TimeStamp, TimeStamp> GetTimeStampsForMarker(
+ const Maybe<const nsAString&>& aStartMark,
+ const Optional<nsAString>& aEndMark,
+ const Maybe<const PerformanceMeasureOptions&>& aOptions,
+ ErrorResult& aRv);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_Performance_h
diff --git a/dom/performance/PerformanceEntry.cpp b/dom/performance/PerformanceEntry.cpp
new file mode 100644
index 0000000000..f9337cc1ce
--- /dev/null
+++ b/dom/performance/PerformanceEntry.cpp
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceEntry.h"
+#include "MainThreadUtils.h"
+
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PerformanceEntry, mParent)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PerformanceEntry)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PerformanceEntry)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceEntry)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+PerformanceEntry::PerformanceEntry(nsISupports* aParent, const nsAString& aName,
+ const nsAString& aEntryType)
+ : mParent(aParent),
+ mName(NS_Atomize(aName)),
+ mEntryType(NS_Atomize(aEntryType)) {}
+
+PerformanceEntry::~PerformanceEntry() = default;
+
+size_t PerformanceEntry::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ // mName and mEntryType are considered to be owned by nsAtomTable.
+ return 0;
+}
+
+size_t PerformanceEntry::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+bool PerformanceEntry::ShouldAddEntryToObserverBuffer(
+ PerformanceObserverInit& aOption) const {
+ if (aOption.mType.WasPassed()) {
+ if (GetEntryType()->Equals(aOption.mType.Value())) {
+ return true;
+ }
+ } else {
+ if (aOption.mEntryTypes.Value().Contains(
+ nsDependentAtomString(GetEntryType()))) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/dom/performance/PerformanceEntry.h b/dom/performance/PerformanceEntry.h
new file mode 100644
index 0000000000..37a13105f9
--- /dev/null
+++ b/dom/performance/PerformanceEntry.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceEntry_h___
+#define mozilla_dom_PerformanceEntry_h___
+
+#include "nsDOMNavigationTiming.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "nsAtom.h"
+#include "mozilla/dom/PerformanceObserverBinding.h"
+
+class nsISupports;
+
+namespace mozilla::dom {
+class PerformanceResourceTiming;
+
+// http://www.w3.org/TR/performance-timeline/#performanceentry
+class PerformanceEntry : public nsISupports, public nsWrapperCache {
+ protected:
+ virtual ~PerformanceEntry();
+
+ public:
+ PerformanceEntry(nsISupports* aParent, const nsAString& aName,
+ const nsAString& aEntryType);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PerformanceEntry)
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ void GetName(nsAString& aName) const {
+ if (mName) {
+ mName->ToString(aName);
+ }
+ }
+
+ const nsAtom* GetName() const { return mName; }
+
+ void GetEntryType(nsAString& aEntryType) const {
+ if (mEntryType) {
+ mEntryType->ToString(aEntryType);
+ }
+ }
+
+ const nsAtom* GetEntryType() const { return mEntryType; }
+
+ void SetEntryType(const nsAString& aEntryType) {
+ mEntryType = NS_Atomize(aEntryType);
+ }
+
+ virtual DOMHighResTimeStamp StartTime() const { return 0; }
+
+ // This is used by the Gecko Profiler only for adding precise markers.
+ // It's not exposed to JS.
+ virtual DOMHighResTimeStamp UnclampedStartTime() const {
+ MOZ_ASSERT(false, "UnclampedStartTime should not be called on this class.");
+ return 0;
+ }
+
+ virtual DOMHighResTimeStamp Duration() const { return 0; }
+
+ virtual const PerformanceResourceTiming* ToResourceTiming() const {
+ return nullptr;
+ }
+
+ virtual bool ShouldAddEntryToObserverBuffer(
+ PerformanceObserverInit& aOption) const;
+
+ virtual void BufferEntryIfNeeded() {}
+
+ virtual size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ protected:
+ virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ nsCOMPtr<nsISupports> mParent;
+ RefPtr<nsAtom> mName;
+ RefPtr<nsAtom> mEntryType;
+};
+
+// Helper classes
+class MOZ_STACK_CLASS PerformanceEntryComparator final {
+ public:
+ bool Equals(const PerformanceEntry* aElem1,
+ const PerformanceEntry* aElem2) const {
+ MOZ_ASSERT(aElem1 && aElem2, "Trying to compare null performance entries");
+ return aElem1->StartTime() == aElem2->StartTime();
+ }
+
+ bool LessThan(const PerformanceEntry* aElem1,
+ const PerformanceEntry* aElem2) const {
+ MOZ_ASSERT(aElem1 && aElem2, "Trying to compare null performance entries");
+ return aElem1->StartTime() < aElem2->StartTime();
+ }
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_PerformanceEntry_h___ */
diff --git a/dom/performance/PerformanceEventTiming.cpp b/dom/performance/PerformanceEventTiming.cpp
new file mode 100644
index 0000000000..7a89329362
--- /dev/null
+++ b/dom/performance/PerformanceEventTiming.cpp
@@ -0,0 +1,207 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceEventTiming.h"
+#include "PerformanceMainThread.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/PerformanceEventTimingBinding.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/Event.h"
+#include "nsContentUtils.h"
+#include "nsIDocShell.h"
+#include <algorithm>
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PerformanceEventTiming, PerformanceEntry,
+ mPerformance, mTarget)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceEventTiming)
+NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry)
+
+NS_IMPL_ADDREF_INHERITED(PerformanceEventTiming, PerformanceEntry)
+NS_IMPL_RELEASE_INHERITED(PerformanceEventTiming, PerformanceEntry)
+
+PerformanceEventTiming::PerformanceEventTiming(Performance* aPerformance,
+ const nsAString& aName,
+ const TimeStamp& aStartTime,
+ bool aIsCacelable,
+ EventMessage aMessage)
+ : PerformanceEntry(aPerformance->GetParentObject(), aName, u"event"_ns),
+ mPerformance(aPerformance),
+ mProcessingStart(aPerformance->NowUnclamped()),
+ mProcessingEnd(0),
+ mStartTime(
+ aPerformance->GetDOMTiming()->TimeStampToDOMHighRes(aStartTime)),
+ mDuration(0),
+ mCancelable(aIsCacelable),
+ mMessage(aMessage) {}
+
+PerformanceEventTiming::PerformanceEventTiming(
+ const PerformanceEventTiming& aEventTimingEntry)
+ : PerformanceEntry(aEventTimingEntry.mPerformance->GetParentObject(),
+ nsDependentAtomString(aEventTimingEntry.GetName()),
+ nsDependentAtomString(aEventTimingEntry.GetEntryType())),
+ mPerformance(aEventTimingEntry.mPerformance),
+ mProcessingStart(aEventTimingEntry.mProcessingStart),
+ mProcessingEnd(aEventTimingEntry.mProcessingEnd),
+ mTarget(aEventTimingEntry.mTarget),
+ mStartTime(aEventTimingEntry.mStartTime),
+ mDuration(aEventTimingEntry.mDuration),
+ mCancelable(aEventTimingEntry.mCancelable),
+ mMessage(aEventTimingEntry.mMessage) {}
+
+JSObject* PerformanceEventTiming::WrapObject(
+ JSContext* cx, JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceEventTiming_Binding::Wrap(cx, this, aGivenProto);
+}
+
+already_AddRefed<PerformanceEventTiming>
+PerformanceEventTiming::TryGenerateEventTiming(const EventTarget* aTarget,
+ const WidgetEvent* aEvent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!StaticPrefs::dom_enable_event_timing() ||
+ aEvent->mFlags.mOnlyChromeDispatch) {
+ return nullptr;
+ }
+
+ if (!aEvent->IsTrusted()) {
+ return nullptr;
+ }
+
+ switch (aEvent->mMessage) {
+ case eMouseAuxClick:
+ case eMouseClick:
+ case eContextMenu:
+ case eMouseDoubleClick:
+ case eMouseDown:
+ case eMouseEnter:
+ case eMouseLeave:
+ case eMouseOut:
+ case eMouseOver:
+ case eMouseUp:
+ case ePointerOver:
+ case ePointerEnter:
+ case ePointerDown:
+ case ePointerUp:
+ case ePointerCancel:
+ case ePointerOut:
+ case ePointerLeave:
+ case ePointerGotCapture:
+ case ePointerLostCapture:
+ case eTouchStart:
+ case eTouchEnd:
+ case eTouchCancel:
+ case eKeyDown:
+ case eKeyPress:
+ case eKeyUp:
+ case eEditorBeforeInput:
+ case eEditorInput:
+ case eCompositionStart:
+ case eCompositionUpdate:
+ case eCompositionEnd:
+ case eDragStart:
+ case eDragEnd:
+ case eDragEnter:
+ case eDragLeave:
+ case eDragOver:
+ case eDrop:
+ break;
+ default:
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> innerWindow =
+ do_QueryInterface(aTarget->GetOwnerGlobal());
+ if (!innerWindow) {
+ return nullptr;
+ }
+
+ if (Performance* performance = innerWindow->GetPerformance()) {
+ const char16_t* eventName = Event::GetEventName(aEvent->mMessage);
+ MOZ_ASSERT(eventName,
+ "User defined events shouldn't be considered as event timing");
+ return RefPtr<PerformanceEventTiming>(
+ new PerformanceEventTiming(
+ performance, nsDependentString(eventName),
+ aEvent->mTimeStamp, aEvent->mFlags.mCancelable,
+ aEvent->mMessage))
+ .forget();
+ }
+ return nullptr;
+}
+
+bool PerformanceEventTiming::ShouldAddEntryToBuffer(double aDuration) const {
+ if (GetEntryType() == nsGkAtoms::firstInput) {
+ return true;
+ }
+ MOZ_ASSERT(GetEntryType() == nsGkAtoms::event);
+ return RawDuration() >= aDuration;
+}
+
+bool PerformanceEventTiming::ShouldAddEntryToObserverBuffer(
+ PerformanceObserverInit& aOption) const {
+ if (!PerformanceEntry::ShouldAddEntryToObserverBuffer(aOption)) {
+ return false;
+ }
+
+ const double minDuration =
+ aOption.mDurationThreshold.WasPassed()
+ ? std::max(aOption.mDurationThreshold.Value(),
+ PerformanceMainThread::kDefaultEventTimingMinDuration)
+ : PerformanceMainThread::kDefaultEventTimingDurationThreshold;
+
+ return ShouldAddEntryToBuffer(minDuration);
+}
+
+void PerformanceEventTiming::BufferEntryIfNeeded() {
+ if (ShouldAddEntryToBuffer(
+ PerformanceMainThread::kDefaultEventTimingDurationThreshold)) {
+ if (GetEntryType() != nsGkAtoms::firstInput) {
+ MOZ_ASSERT(GetEntryType() == nsGkAtoms::event);
+ mPerformance->BufferEventTimingEntryIfNeeded(this);
+ }
+ }
+}
+
+nsINode* PerformanceEventTiming::GetTarget() const {
+ nsCOMPtr<Element> element = do_QueryReferent(mTarget);
+ if (!element) {
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> global =
+ do_QueryInterface(element->GetOwnerGlobal());
+ if (!global) {
+ return nullptr;
+ }
+ return nsContentUtils::GetAnElementForTiming(element, global->GetExtantDoc(),
+ mPerformance->GetParentObject());
+}
+
+void PerformanceEventTiming::FinalizeEventTiming(EventTarget* aTarget) {
+ if (!aTarget) {
+ return;
+ }
+ nsCOMPtr<nsPIDOMWindowInner> global =
+ do_QueryInterface(aTarget->GetOwnerGlobal());
+ if (!global) {
+ return;
+ }
+
+ mProcessingEnd = mPerformance->NowUnclamped();
+
+ Element* element = Element::FromEventTarget(aTarget);
+ if (!element || element->ChromeOnlyAccess()) {
+ return;
+ }
+
+ mTarget = do_GetWeakReference(element);
+
+ mPerformance->InsertEventTimingEntry(this);
+}
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceEventTiming.h b/dom/performance/PerformanceEventTiming.h
new file mode 100644
index 0000000000..3f39610d95
--- /dev/null
+++ b/dom/performance/PerformanceEventTiming.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceEventTiming_h___
+#define mozilla_dom_PerformanceEventTiming_h___
+
+#include "mozilla/dom/PerformanceEntry.h"
+#include "mozilla/EventForwards.h"
+#include "nsRFPService.h"
+#include "Performance.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsINode.h"
+
+namespace mozilla {
+class WidgetEvent;
+namespace dom {
+
+class PerformanceEventTiming final
+ : public PerformanceEntry,
+ public LinkedListElement<RefPtr<PerformanceEventTiming>> {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PerformanceEventTiming,
+ PerformanceEntry)
+
+ static already_AddRefed<PerformanceEventTiming> TryGenerateEventTiming(
+ const EventTarget* aTarget, const WidgetEvent* aEvent);
+
+ already_AddRefed<PerformanceEventTiming> Clone() {
+ RefPtr<PerformanceEventTiming> eventTiming =
+ new PerformanceEventTiming(*this);
+ return eventTiming.forget();
+ }
+
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ DOMHighResTimeStamp ProcessingStart() const {
+ if (mCachedProcessingStart.isNothing()) {
+ mCachedProcessingStart.emplace(nsRFPService::ReduceTimePrecisionAsMSecs(
+ mProcessingStart, mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType()));
+ }
+ return mCachedProcessingStart.value();
+ }
+
+ DOMHighResTimeStamp ProcessingEnd() const {
+ if (mCachedProcessingEnd.isNothing()) {
+ mCachedProcessingEnd.emplace(nsRFPService::ReduceTimePrecisionAsMSecs(
+ mProcessingEnd, mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType()));
+ }
+ return mCachedProcessingEnd.value();
+ }
+
+ bool Cancelable() const { return mCancelable; }
+
+ nsINode* GetTarget() const;
+
+ void SetDuration(const DOMHighResTimeStamp aDuration) {
+ mDuration = aDuration;
+ }
+
+ // nsRFPService::ReduceTimePrecisionAsMSecs might causes
+ // some memory overhead, using the raw timestamp internally
+ // to avoid calling in unnecessarily.
+ DOMHighResTimeStamp RawDuration() const { return mDuration; }
+
+ DOMHighResTimeStamp Duration() const override {
+ if (mCachedDuration.isNothing()) {
+ mCachedDuration.emplace(nsRFPService::ReduceTimePrecisionAsMSecs(
+ mDuration, mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType()));
+ }
+ return mCachedDuration.value();
+ }
+
+ // Similar as RawDuration; Used to avoid calling
+ // nsRFPService::ReduceTimePrecisionAsMSecs unnecessarily.
+ DOMHighResTimeStamp RawStartTime() const { return mStartTime; }
+
+ DOMHighResTimeStamp StartTime() const override {
+ if (mCachedStartTime.isNothing()) {
+ mCachedStartTime.emplace(nsRFPService::ReduceTimePrecisionAsMSecs(
+ mStartTime, mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType()));
+ }
+ return mCachedStartTime.value();
+ }
+
+ bool ShouldAddEntryToBuffer(double aDuration) const;
+ bool ShouldAddEntryToObserverBuffer(PerformanceObserverInit&) const override;
+
+ void BufferEntryIfNeeded() override;
+
+ void FinalizeEventTiming(EventTarget* aTarget);
+
+ EventMessage GetMessage() const { return mMessage; }
+
+ private:
+ PerformanceEventTiming(Performance* aPerformance, const nsAString& aName,
+ const TimeStamp& aStartTime, bool aIsCacelable,
+ EventMessage aMessage);
+
+ PerformanceEventTiming(const PerformanceEventTiming& aEventTimingEntry);
+
+ ~PerformanceEventTiming() = default;
+
+ RefPtr<Performance> mPerformance;
+
+ DOMHighResTimeStamp mProcessingStart;
+ mutable Maybe<DOMHighResTimeStamp> mCachedProcessingStart;
+
+ DOMHighResTimeStamp mProcessingEnd;
+ mutable Maybe<DOMHighResTimeStamp> mCachedProcessingEnd;
+
+ nsWeakPtr mTarget;
+
+ DOMHighResTimeStamp mStartTime;
+ mutable Maybe<DOMHighResTimeStamp> mCachedStartTime;
+
+ DOMHighResTimeStamp mDuration;
+ mutable Maybe<DOMHighResTimeStamp> mCachedDuration;
+
+ bool mCancelable;
+
+ EventMessage mMessage;
+};
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/performance/PerformanceMainThread.cpp b/dom/performance/PerformanceMainThread.cpp
new file mode 100644
index 0000000000..324f944d50
--- /dev/null
+++ b/dom/performance/PerformanceMainThread.cpp
@@ -0,0 +1,756 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceMainThread.h"
+#include "PerformanceNavigation.h"
+#include "PerformancePaintTiming.h"
+#include "jsapi.h"
+#include "js/GCAPI.h"
+#include "js/PropertyAndElement.h" // JS_DefineProperty
+#include "mozilla/HoldDropJSObjects.h"
+#include "PerformanceEventTiming.h"
+#include "LargestContentfulPaint.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventCounts.h"
+#include "mozilla/dom/PerformanceEventTimingBinding.h"
+#include "mozilla/dom/PerformanceNavigationTiming.h"
+#include "mozilla/dom/PerformanceResourceTiming.h"
+#include "mozilla/dom/PerformanceTiming.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_privacy.h"
+#include "mozilla/PresShell.h"
+#include "nsIChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIDocShell.h"
+#include "nsTextFrame.h"
+#include "nsContainerFrame.h"
+
+namespace mozilla::dom {
+
+extern mozilla::LazyLogModule gLCPLogging;
+
+namespace {
+
+void GetURLSpecFromChannel(nsITimedChannel* aChannel, nsAString& aSpec) {
+ aSpec.AssignLiteral("document");
+
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aChannel);
+ if (!channel) {
+ return;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = channel->GetURI(getter_AddRefs(uri));
+ if (NS_WARN_IF(NS_FAILED(rv)) || !uri) {
+ return;
+ }
+
+ nsAutoCString spec;
+ rv = uri->GetSpec(spec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ CopyUTF8toUTF16(spec, aSpec);
+}
+
+} // namespace
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PerformanceMainThread)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PerformanceMainThread,
+ Performance)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(
+ mTiming, mNavigation, mDocEntry, mFCPTiming, mEventTimingEntries,
+ mLargestContentfulPaintEntries, mFirstInputEvent, mPendingPointerDown,
+ mPendingEventTimingEntries, mEventCounts)
+ tmp->mImageLCPEntryMap.Clear();
+ tmp->mTextFrameUnions.Clear();
+ mozilla::DropJSObjects(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PerformanceMainThread,
+ Performance)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
+ mTiming, mNavigation, mDocEntry, mFCPTiming, mEventTimingEntries,
+ mLargestContentfulPaintEntries, mFirstInputEvent, mPendingPointerDown,
+ mPendingEventTimingEntries, mEventCounts, mImageLCPEntryMap,
+ mTextFrameUnions)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceMainThread,
+ Performance)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMozMemory)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_ADDREF_INHERITED(PerformanceMainThread, Performance)
+NS_IMPL_RELEASE_INHERITED(PerformanceMainThread, Performance)
+
+// QueryInterface implementation for PerformanceMainThread
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceMainThread)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, EventTarget)
+NS_INTERFACE_MAP_END_INHERITING(Performance)
+
+PerformanceMainThread::PerformanceMainThread(nsPIDOMWindowInner* aWindow,
+ nsDOMNavigationTiming* aDOMTiming,
+ nsITimedChannel* aChannel)
+ : Performance(aWindow->AsGlobal()),
+ mDOMTiming(aDOMTiming),
+ mChannel(aChannel) {
+ MOZ_ASSERT(aWindow, "Parent window object should be provided");
+ if (StaticPrefs::dom_enable_event_timing()) {
+ mEventCounts = new class EventCounts(GetParentObject());
+ }
+ CreateNavigationTimingEntry();
+
+ if (StaticPrefs::dom_enable_largest_contentful_paint()) {
+ nsCOMPtr<nsPIDOMWindowInner> owner = GetOwner();
+ MarkerInnerWindowId innerWindowID =
+ owner ? MarkerInnerWindowId(owner->WindowID())
+ : MarkerInnerWindowId::NoId();
+ // There might be multiple LCP entries and we only care about the latest one
+ // which is also the biggest value. That's why we need to record these
+ // markers in two different places:
+ // - During the Document unload, so we can record the closed pages.
+ // - During the profile capture, so we can record the open pages.
+ // We are capturing the second one here.
+ // Our static analysis doesn't allow capturing ref-counted pointers in
+ // lambdas, so we need to hide it in a uintptr_t. This is safe because this
+ // lambda will be destroyed in ~PerformanceMainThread().
+ uintptr_t self = reinterpret_cast<uintptr_t>(this);
+ profiler_add_state_change_callback(
+ // Using the "Pausing" state as "GeneratingProfile" profile happens too
+ // late; we can not record markers if the profiler is already paused.
+ ProfilingState::Pausing,
+ [self, innerWindowID](ProfilingState aProfilingState) {
+ const PerformanceMainThread* selfPtr =
+ reinterpret_cast<const PerformanceMainThread*>(self);
+
+ selfPtr->GetDOMTiming()->MaybeAddLCPProfilerMarker(innerWindowID);
+ },
+ self);
+ }
+}
+
+PerformanceMainThread::~PerformanceMainThread() {
+ profiler_remove_state_change_callback(reinterpret_cast<uintptr_t>(this));
+ mozilla::DropJSObjects(this);
+}
+
+void PerformanceMainThread::GetMozMemory(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aObj) {
+ if (!mMozMemory) {
+ JS::Rooted<JSObject*> mozMemoryObj(aCx, JS_NewPlainObject(aCx));
+ JS::Rooted<JSObject*> gcMemoryObj(aCx, js::gc::NewMemoryInfoObject(aCx));
+ if (!mozMemoryObj || !gcMemoryObj) {
+ MOZ_CRASH("out of memory creating performance.mozMemory");
+ }
+ if (!JS_DefineProperty(aCx, mozMemoryObj, "gc", gcMemoryObj,
+ JSPROP_ENUMERATE)) {
+ MOZ_CRASH("out of memory creating performance.mozMemory");
+ }
+ mMozMemory = mozMemoryObj;
+ mozilla::HoldJSObjects(this);
+ }
+
+ aObj.set(mMozMemory);
+}
+
+PerformanceTiming* PerformanceMainThread::Timing() {
+ if (!mTiming) {
+ // For navigation timing, the third argument (an nsIHttpChannel) is null
+ // since the cross-domain redirect were already checked. The last
+ // argument (zero time) for performance.timing is the navigation start
+ // value.
+ mTiming = new PerformanceTiming(this, mChannel, nullptr,
+ mDOMTiming->GetNavigationStart());
+ }
+
+ return mTiming;
+}
+
+void PerformanceMainThread::DispatchBufferFullEvent() {
+ RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
+ // it bubbles, and it isn't cancelable
+ event->InitEvent(u"resourcetimingbufferfull"_ns, true, false);
+ event->SetTrusted(true);
+ DispatchEvent(*event);
+}
+
+PerformanceNavigation* PerformanceMainThread::Navigation() {
+ if (!mNavigation) {
+ mNavigation = new PerformanceNavigation(this);
+ }
+
+ return mNavigation;
+}
+
+/**
+ * An entry should be added only after the resource is loaded.
+ * This method is not thread safe and can only be called on the main thread.
+ */
+void PerformanceMainThread::AddEntry(nsIHttpChannel* channel,
+ nsITimedChannel* timedChannel) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoString initiatorType;
+ nsAutoString entryName;
+
+ UniquePtr<PerformanceTimingData> performanceTimingData(
+ PerformanceTimingData::Create(timedChannel, channel, 0, initiatorType,
+ entryName));
+ if (!performanceTimingData) {
+ return;
+ }
+ AddRawEntry(std::move(performanceTimingData), initiatorType, entryName);
+}
+
+void PerformanceMainThread::AddEntry(const nsString& entryName,
+ const nsString& initiatorType,
+ UniquePtr<PerformanceTimingData>&& aData) {
+ AddRawEntry(std::move(aData), initiatorType, entryName);
+}
+
+void PerformanceMainThread::AddRawEntry(UniquePtr<PerformanceTimingData> aData,
+ const nsAString& aInitiatorType,
+ const nsAString& aEntryName) {
+ // The PerformanceResourceTiming object will use the PerformanceTimingData
+ // object to get all the required timings.
+ auto entry =
+ MakeRefPtr<PerformanceResourceTiming>(std::move(aData), this, aEntryName);
+ entry->SetInitiatorType(aInitiatorType);
+ InsertResourceEntry(entry);
+}
+
+void PerformanceMainThread::SetFCPTimingEntry(PerformancePaintTiming* aEntry) {
+ MOZ_ASSERT(aEntry);
+ if (!mFCPTiming) {
+ mFCPTiming = aEntry;
+ QueueEntry(aEntry);
+ }
+}
+
+void PerformanceMainThread::InsertEventTimingEntry(
+ PerformanceEventTiming* aEventEntry) {
+ mPendingEventTimingEntries.insertBack(aEventEntry);
+
+ if (mHasQueuedRefreshdriverObserver) {
+ return;
+ }
+
+ PresShell* presShell = GetPresShell();
+ if (!presShell) {
+ return;
+ }
+
+ nsPresContext* presContext = presShell->GetPresContext();
+ if (!presContext) {
+ return;
+ }
+
+ // Using PostRefreshObserver is fine because we don't
+ // run any JS between the `mark paint timing` step and the
+ // `pending Event Timing entries` step. So mixing the order
+ // here is fine.
+ mHasQueuedRefreshdriverObserver = true;
+ presContext->RegisterManagedPostRefreshObserver(
+ new ManagedPostRefreshObserver(
+ presContext, [performance = RefPtr<PerformanceMainThread>(this)](
+ bool aWasCanceled) {
+ if (!aWasCanceled) {
+ // XXX Should we do this even if canceled?
+ performance->DispatchPendingEventTimingEntries();
+ }
+ performance->mHasQueuedRefreshdriverObserver = false;
+ return ManagedPostRefreshObserver::Unregister::Yes;
+ }));
+}
+
+void PerformanceMainThread::BufferEventTimingEntryIfNeeded(
+ PerformanceEventTiming* aEventEntry) {
+ if (mEventTimingEntries.Length() < kDefaultEventTimingBufferSize) {
+ mEventTimingEntries.AppendElement(aEventEntry);
+ }
+}
+
+void PerformanceMainThread::BufferLargestContentfulPaintEntryIfNeeded(
+ LargestContentfulPaint* aEntry) {
+ MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint());
+ if (mLargestContentfulPaintEntries.Length() <
+ kMaxLargestContentfulPaintBufferSize) {
+ mLargestContentfulPaintEntries.AppendElement(aEntry);
+ }
+}
+
+void PerformanceMainThread::DispatchPendingEventTimingEntries() {
+ DOMHighResTimeStamp renderingTime = NowUnclamped();
+
+ while (!mPendingEventTimingEntries.isEmpty()) {
+ RefPtr<PerformanceEventTiming> entry =
+ mPendingEventTimingEntries.popFirst();
+
+ entry->SetDuration(renderingTime - entry->RawStartTime());
+ IncEventCount(entry->GetName());
+
+ if (entry->RawDuration() >= kDefaultEventTimingMinDuration) {
+ QueueEntry(entry);
+ }
+
+ if (!mHasDispatchedInputEvent) {
+ switch (entry->GetMessage()) {
+ case ePointerDown: {
+ mPendingPointerDown = entry->Clone();
+ mPendingPointerDown->SetEntryType(u"first-input"_ns);
+ break;
+ }
+ case ePointerUp: {
+ if (mPendingPointerDown) {
+ MOZ_ASSERT(!mFirstInputEvent);
+ mFirstInputEvent = mPendingPointerDown.forget();
+ QueueEntry(mFirstInputEvent);
+ SetHasDispatchedInputEvent();
+ }
+ break;
+ }
+ case eMouseClick:
+ case eKeyDown:
+ case eMouseDown: {
+ mFirstInputEvent = entry->Clone();
+ mFirstInputEvent->SetEntryType(u"first-input"_ns);
+ QueueEntry(mFirstInputEvent);
+ SetHasDispatchedInputEvent();
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+}
+
+DOMHighResTimeStamp PerformanceMainThread::GetPerformanceTimingFromString(
+ const nsAString& aProperty) {
+ // ::Measure expects the values returned from this function to be passed
+ // through ReduceTimePrecision already.
+ if (!IsPerformanceTimingAttribute(aProperty)) {
+ return 0;
+ }
+ // Values from Timing() are already reduced
+ if (aProperty.EqualsLiteral("redirectStart")) {
+ return Timing()->RedirectStart();
+ }
+ if (aProperty.EqualsLiteral("redirectEnd")) {
+ return Timing()->RedirectEnd();
+ }
+ if (aProperty.EqualsLiteral("fetchStart")) {
+ return Timing()->FetchStart();
+ }
+ if (aProperty.EqualsLiteral("domainLookupStart")) {
+ return Timing()->DomainLookupStart();
+ }
+ if (aProperty.EqualsLiteral("domainLookupEnd")) {
+ return Timing()->DomainLookupEnd();
+ }
+ if (aProperty.EqualsLiteral("connectStart")) {
+ return Timing()->ConnectStart();
+ }
+ if (aProperty.EqualsLiteral("secureConnectionStart")) {
+ return Timing()->SecureConnectionStart();
+ }
+ if (aProperty.EqualsLiteral("connectEnd")) {
+ return Timing()->ConnectEnd();
+ }
+ if (aProperty.EqualsLiteral("requestStart")) {
+ return Timing()->RequestStart();
+ }
+ if (aProperty.EqualsLiteral("responseStart")) {
+ return Timing()->ResponseStart();
+ }
+ if (aProperty.EqualsLiteral("responseEnd")) {
+ return Timing()->ResponseEnd();
+ }
+ // Values from GetDOMTiming() are not.
+ DOMHighResTimeStamp retValue;
+ if (aProperty.EqualsLiteral("navigationStart")) {
+ // DOMHighResTimeStamp is in relation to navigationStart, so this will be
+ // zero.
+ retValue = GetDOMTiming()->GetNavigationStart();
+ } else if (aProperty.EqualsLiteral("unloadEventStart")) {
+ retValue = GetDOMTiming()->GetUnloadEventStart();
+ } else if (aProperty.EqualsLiteral("unloadEventEnd")) {
+ retValue = GetDOMTiming()->GetUnloadEventEnd();
+ } else if (aProperty.EqualsLiteral("domLoading")) {
+ retValue = GetDOMTiming()->GetDomLoading();
+ } else if (aProperty.EqualsLiteral("domInteractive")) {
+ retValue = GetDOMTiming()->GetDomInteractive();
+ } else if (aProperty.EqualsLiteral("domContentLoadedEventStart")) {
+ retValue = GetDOMTiming()->GetDomContentLoadedEventStart();
+ } else if (aProperty.EqualsLiteral("domContentLoadedEventEnd")) {
+ retValue = GetDOMTiming()->GetDomContentLoadedEventEnd();
+ } else if (aProperty.EqualsLiteral("domComplete")) {
+ retValue = GetDOMTiming()->GetDomComplete();
+ } else if (aProperty.EqualsLiteral("loadEventStart")) {
+ retValue = GetDOMTiming()->GetLoadEventStart();
+ } else if (aProperty.EqualsLiteral("loadEventEnd")) {
+ retValue = GetDOMTiming()->GetLoadEventEnd();
+ } else {
+ MOZ_CRASH(
+ "IsPerformanceTimingAttribute and GetPerformanceTimingFromString are "
+ "out "
+ "of sync");
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ retValue, GetRandomTimelineSeed(), mRTPCallerType);
+}
+
+void PerformanceMainThread::InsertUserEntry(PerformanceEntry* aEntry) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString uri;
+ double markCreationEpoch = 0;
+
+ if (StaticPrefs::dom_performance_enable_user_timing_logging() ||
+ StaticPrefs::dom_performance_enable_notify_performance_timing()) {
+ nsresult rv = NS_ERROR_FAILURE;
+ nsCOMPtr<nsPIDOMWindowInner> owner = GetOwner();
+ if (owner && owner->GetDocumentURI()) {
+ rv = owner->GetDocumentURI()->GetHost(uri);
+ }
+
+ if (NS_FAILED(rv)) {
+ // If we have no URI, just put in "none".
+ uri.AssignLiteral("none");
+ }
+
+ // PR_Now() returns a signed 64-bit integer. Since it represents a
+ // timestamp, only ~32-bits will represent the value which should safely fit
+ // into a double.
+ markCreationEpoch = static_cast<double>(PR_Now() / PR_USEC_PER_MSEC);
+
+ if (StaticPrefs::dom_performance_enable_user_timing_logging()) {
+ Performance::LogEntry(aEntry, uri);
+ }
+ }
+
+ if (StaticPrefs::dom_performance_enable_notify_performance_timing()) {
+ TimingNotification(aEntry, uri, markCreationEpoch);
+ }
+
+ Performance::InsertUserEntry(aEntry);
+}
+
+TimeStamp PerformanceMainThread::CreationTimeStamp() const {
+ return GetDOMTiming()->GetNavigationStartTimeStamp();
+}
+
+DOMHighResTimeStamp PerformanceMainThread::CreationTime() const {
+ return GetDOMTiming()->GetNavigationStart();
+}
+
+void PerformanceMainThread::CreateNavigationTimingEntry() {
+ MOZ_ASSERT(!mDocEntry, "mDocEntry should be null.");
+
+ if (!StaticPrefs::dom_enable_performance_navigation_timing()) {
+ return;
+ }
+
+ nsAutoString name;
+ GetURLSpecFromChannel(mChannel, name);
+
+ UniquePtr<PerformanceTimingData> timing(
+ new PerformanceTimingData(mChannel, nullptr, 0));
+
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ timing->SetPropertiesFromHttpChannel(httpChannel, mChannel);
+ }
+
+ mDocEntry = new PerformanceNavigationTiming(std::move(timing), this, name);
+}
+
+void PerformanceMainThread::UpdateNavigationTimingEntry() {
+ if (!mDocEntry) {
+ return;
+ }
+
+ // Let's update some values.
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
+ if (httpChannel) {
+ mDocEntry->UpdatePropertiesFromHttpChannel(httpChannel, mChannel);
+ }
+}
+
+void PerformanceMainThread::QueueNavigationTimingEntry() {
+ if (!mDocEntry) {
+ return;
+ }
+
+ UpdateNavigationTimingEntry();
+
+ QueueEntry(mDocEntry);
+}
+
+void PerformanceMainThread::QueueLargestContentfulPaintEntry(
+ LargestContentfulPaint* aEntry) {
+ MOZ_ASSERT(StaticPrefs::dom_enable_largest_contentful_paint());
+ QueueEntry(aEntry);
+}
+
+EventCounts* PerformanceMainThread::EventCounts() {
+ MOZ_ASSERT(StaticPrefs::dom_enable_event_timing());
+ return mEventCounts;
+}
+
+void PerformanceMainThread::GetEntries(
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ aRetval = mResourceEntries.Clone();
+ aRetval.AppendElements(mUserEntries);
+
+ if (mDocEntry) {
+ aRetval.AppendElement(mDocEntry);
+ }
+
+ if (mFCPTiming) {
+ aRetval.AppendElement(mFCPTiming);
+ }
+ aRetval.Sort(PerformanceEntryComparator());
+}
+
+void PerformanceMainThread::GetEntriesByType(
+ const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ RefPtr<nsAtom> type = NS_Atomize(aEntryType);
+ if (type == nsGkAtoms::navigation) {
+ aRetval.Clear();
+
+ if (mDocEntry) {
+ aRetval.AppendElement(mDocEntry);
+ }
+ return;
+ }
+
+ if (type == nsGkAtoms::paint) {
+ if (mFCPTiming) {
+ aRetval.AppendElement(mFCPTiming);
+ return;
+ }
+ }
+
+ if (type == nsGkAtoms::firstInput && mFirstInputEvent) {
+ aRetval.AppendElement(mFirstInputEvent);
+ return;
+ }
+
+ Performance::GetEntriesByType(aEntryType, aRetval);
+}
+void PerformanceMainThread::GetEntriesByTypeForObserver(
+ const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ if (aEntryType.EqualsLiteral("event")) {
+ aRetval.AppendElements(mEventTimingEntries);
+ return;
+ }
+
+ if (StaticPrefs::dom_enable_largest_contentful_paint()) {
+ if (aEntryType.EqualsLiteral("largest-contentful-paint")) {
+ aRetval.AppendElements(mLargestContentfulPaintEntries);
+ return;
+ }
+ }
+
+ return GetEntriesByType(aEntryType, aRetval);
+}
+
+void PerformanceMainThread::GetEntriesByName(
+ const nsAString& aName, const Optional<nsAString>& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ Performance::GetEntriesByName(aName, aEntryType, aRetval);
+
+ if (mFCPTiming && mFCPTiming->GetName()->Equals(aName) &&
+ (!aEntryType.WasPassed() ||
+ mFCPTiming->GetEntryType()->Equals(aEntryType.Value()))) {
+ aRetval.AppendElement(mFCPTiming);
+ return;
+ }
+
+ // The navigation entry is the first one. If it exists and the name matches,
+ // let put it in front.
+ if (mDocEntry && mDocEntry->GetName()->Equals(aName)) {
+ aRetval.InsertElementAt(0, mDocEntry);
+ return;
+ }
+}
+
+mozilla::PresShell* PerformanceMainThread::GetPresShell() {
+ nsIGlobalObject* ownerGlobal = GetOwnerGlobal();
+ if (!ownerGlobal) {
+ return nullptr;
+ }
+ if (Document* doc = ownerGlobal->GetAsInnerWindow()->GetExtantDoc()) {
+ return doc->GetPresShell();
+ }
+ return nullptr;
+}
+
+void PerformanceMainThread::IncEventCount(const nsAtom* aType) {
+ MOZ_ASSERT(StaticPrefs::dom_enable_event_timing());
+
+ // This occurs when the pref was false when the performance
+ // object was first created, and became true later. It's
+ // okay to return early because eventCounts is not exposed.
+ if (!mEventCounts) {
+ return;
+ }
+
+ ErrorResult rv;
+ uint64_t count = EventCounts_Binding::MaplikeHelpers::Get(
+ mEventCounts, nsDependentAtomString(aType), rv);
+ MOZ_ASSERT(!rv.Failed());
+ EventCounts_Binding::MaplikeHelpers::Set(
+ mEventCounts, nsDependentAtomString(aType), ++count, rv);
+ MOZ_ASSERT(!rv.Failed());
+}
+
+size_t PerformanceMainThread::SizeOfEventEntries(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t eventEntries = 0;
+ for (const PerformanceEventTiming* entry : mEventTimingEntries) {
+ eventEntries += entry->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return eventEntries;
+}
+
+void PerformanceMainThread::ProcessElementTiming() {
+ if (!StaticPrefs::dom_enable_largest_contentful_paint()) {
+ return;
+ }
+ const bool shouldLCPDataEmpty =
+ HasDispatchedInputEvent() || HasDispatchedScrollEvent();
+ MOZ_ASSERT_IF(shouldLCPDataEmpty,
+ mTextFrameUnions.IsEmpty() && mImageLCPEntryMap.IsEmpty());
+
+ if (shouldLCPDataEmpty) {
+ return;
+ }
+
+ nsPresContext* presContext = GetPresShell()->GetPresContext();
+ MOZ_ASSERT(presContext);
+
+ // After https://github.com/w3c/largest-contentful-paint/issues/104 is
+ // resolved, LargestContentfulPaint and FirstContentfulPaint should
+ // be using the same timestamp, which should be the same timestamp
+ // as to what https://w3c.github.io/paint-timing/#mark-paint-timing step 2
+ // defines.
+ // TODO(sefeng): Check the timestamp after this issue is resolved.
+ TimeStamp rawNowTime = presContext->GetMarkPaintTimingStart();
+
+ MOZ_ASSERT(GetOwnerGlobal());
+ Document* document = GetOwnerGlobal()->GetAsInnerWindow()->GetExtantDoc();
+ if (!document ||
+ !nsContentUtils::GetInProcessSubtreeRootDocument(document)->IsActive()) {
+ return;
+ }
+
+ nsTArray<ImagePendingRendering> imagesPendingRendering =
+ std::move(mImagesPendingRendering);
+ for (const auto& imagePendingRendering : imagesPendingRendering) {
+ RefPtr<Element> element = imagePendingRendering.GetElement();
+ if (!element) {
+ continue;
+ }
+
+ MOZ_ASSERT(imagePendingRendering.mLoadTime <= rawNowTime);
+ if (imgRequestProxy* requestProxy =
+ imagePendingRendering.GetImgRequestProxy()) {
+ LCPHelpers::CreateLCPEntryForImage(
+ this, element, requestProxy, imagePendingRendering.mLoadTime,
+ rawNowTime, imagePendingRendering.mLCPImageEntryKey);
+ }
+ }
+
+ MOZ_ASSERT(mImagesPendingRendering.IsEmpty());
+}
+
+void PerformanceMainThread::FinalizeLCPEntriesForText() {
+ nsPresContext* presContext = GetPresShell()->GetPresContext();
+ MOZ_ASSERT(presContext);
+
+ bool canFinalize = StaticPrefs::dom_enable_largest_contentful_paint() &&
+ !presContext->HasStoppedGeneratingLCP();
+ nsTHashMap<nsRefPtrHashKey<Element>, nsRect> textFrameUnion =
+ std::move(GetTextFrameUnions());
+ if (canFinalize) {
+ for (const auto& textFrameUnion : textFrameUnion) {
+ LCPHelpers::FinalizeLCPEntryForText(
+ this, presContext->GetMarkPaintTimingStart(), textFrameUnion.GetKey(),
+ textFrameUnion.GetData(), presContext);
+ }
+ }
+ MOZ_ASSERT(GetTextFrameUnions().IsEmpty());
+}
+
+void PerformanceMainThread::StoreImageLCPEntry(
+ Element* aElement, imgRequestProxy* aImgRequestProxy,
+ LargestContentfulPaint* aEntry) {
+ mImageLCPEntryMap.InsertOrUpdate({aElement, aImgRequestProxy}, aEntry);
+}
+
+already_AddRefed<LargestContentfulPaint>
+PerformanceMainThread::GetImageLCPEntry(Element* aElement,
+ imgRequestProxy* aImgRequestProxy) {
+ Maybe<RefPtr<LargestContentfulPaint>> entry =
+ mImageLCPEntryMap.Extract({aElement, aImgRequestProxy});
+ if (entry.isNothing()) {
+ return nullptr;
+ }
+
+ Document* doc = aElement->GetComposedDoc();
+ MOZ_ASSERT(doc, "Element should be connected when it's painted");
+
+ Maybe<LCPImageEntryKey>& contentIdentifier =
+ entry.value()->GetLCPImageEntryKey();
+ if (contentIdentifier.isSome()) {
+ doc->ContentIdentifiersForLCP().EnsureRemoved(contentIdentifier.value());
+ contentIdentifier.reset();
+ }
+
+ return entry.value().forget();
+}
+
+bool PerformanceMainThread::UpdateLargestContentfulPaintSize(double aSize) {
+ if (aSize > mLargestContentfulPaintSize) {
+ mLargestContentfulPaintSize = aSize;
+ return true;
+ }
+ return false;
+}
+
+void PerformanceMainThread::SetHasDispatchedScrollEvent() {
+ mHasDispatchedScrollEvent = true;
+ ClearGeneratedTempDataForLCP();
+}
+
+void PerformanceMainThread::SetHasDispatchedInputEvent() {
+ mHasDispatchedInputEvent = true;
+ ClearGeneratedTempDataForLCP();
+}
+
+void PerformanceMainThread::ClearGeneratedTempDataForLCP() {
+ mTextFrameUnions.Clear();
+ mImageLCPEntryMap.Clear();
+ mImagesPendingRendering.Clear();
+
+ nsIGlobalObject* ownerGlobal = GetOwnerGlobal();
+ if (!ownerGlobal) {
+ return;
+ }
+
+ if (Document* document = ownerGlobal->GetAsInnerWindow()->GetExtantDoc()) {
+ document->ContentIdentifiersForLCP().Clear();
+ }
+}
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceMainThread.h b/dom/performance/PerformanceMainThread.h
new file mode 100644
index 0000000000..46d7a339d1
--- /dev/null
+++ b/dom/performance/PerformanceMainThread.h
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceMainThread_h
+#define mozilla_dom_PerformanceMainThread_h
+
+#include "Performance.h"
+#include "PerformanceStorage.h"
+#include "LargestContentfulPaint.h"
+#include "nsTextFrame.h"
+
+namespace mozilla::dom {
+
+class PerformanceNavigationTiming;
+class PerformanceEventTiming;
+
+using ImageLCPEntryMap =
+ nsTHashMap<LCPEntryHashEntry, RefPtr<LargestContentfulPaint>>;
+
+using TextFrameUnions = nsTHashMap<nsRefPtrHashKey<Element>, nsRect>;
+
+class PerformanceMainThread final : public Performance,
+ public PerformanceStorage {
+ public:
+ PerformanceMainThread(nsPIDOMWindowInner* aWindow,
+ nsDOMNavigationTiming* aDOMTiming,
+ nsITimedChannel* aChannel);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(PerformanceMainThread,
+ Performance)
+
+ PerformanceStorage* AsPerformanceStorage() override { return this; }
+
+ virtual PerformanceTiming* Timing() override;
+
+ virtual PerformanceNavigation* Navigation() override;
+
+ virtual void AddEntry(nsIHttpChannel* channel,
+ nsITimedChannel* timedChannel) override;
+
+ // aData must be non-null.
+ virtual void AddEntry(const nsString& entryName,
+ const nsString& initiatorType,
+ UniquePtr<PerformanceTimingData>&& aData) override;
+
+ // aPerformanceTimingData must be non-null.
+ void AddRawEntry(UniquePtr<PerformanceTimingData> aPerformanceTimingData,
+ const nsAString& aInitiatorType,
+ const nsAString& aEntryName);
+ virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) override;
+ bool HadFCPTimingEntry() const { return mFCPTiming; }
+
+ void InsertEventTimingEntry(PerformanceEventTiming*) override;
+ void BufferEventTimingEntryIfNeeded(PerformanceEventTiming*) override;
+ void DispatchPendingEventTimingEntries() override;
+
+ void BufferLargestContentfulPaintEntryIfNeeded(LargestContentfulPaint*);
+
+ TimeStamp CreationTimeStamp() const override;
+
+ DOMHighResTimeStamp CreationTime() const override;
+
+ virtual void GetMozMemory(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aObj) override;
+
+ virtual nsDOMNavigationTiming* GetDOMTiming() const override {
+ return mDOMTiming;
+ }
+
+ virtual uint64_t GetRandomTimelineSeed() override {
+ return GetDOMTiming()->GetRandomTimelineSeed();
+ }
+
+ virtual nsITimedChannel* GetChannel() const override { return mChannel; }
+
+ // The GetEntries* methods need to be overriden in order to add the
+ // the document entry of type navigation.
+ virtual void GetEntries(nsTArray<RefPtr<PerformanceEntry>>& aRetval) override;
+
+ // Return entries which qualify availableFromTimeline boolean check
+ virtual void GetEntriesByType(
+ const nsAString& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) override;
+
+ // There are entries that we don't want expose via performance, however
+ // we do want PerformanceObserver to get them
+ void GetEntriesByTypeForObserver(
+ const nsAString& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) override;
+ virtual void GetEntriesByName(
+ const nsAString& aName, const Optional<nsAString>& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) override;
+
+ void UpdateNavigationTimingEntry() override;
+ void QueueNavigationTimingEntry() override;
+ void QueueLargestContentfulPaintEntry(LargestContentfulPaint* aEntry);
+
+ size_t SizeOfEventEntries(mozilla::MallocSizeOf aMallocSizeOf) const override;
+
+ static constexpr uint32_t kDefaultEventTimingBufferSize = 150;
+ static constexpr uint32_t kDefaultEventTimingDurationThreshold = 104;
+ static constexpr double kDefaultEventTimingMinDuration = 16.0;
+
+ static constexpr uint32_t kMaxLargestContentfulPaintBufferSize = 150;
+
+ class EventCounts* EventCounts() override;
+
+ bool IsGlobalObjectWindow() const override { return true; };
+
+ bool HasDispatchedInputEvent() const { return mHasDispatchedInputEvent; }
+
+ void SetHasDispatchedScrollEvent();
+ bool HasDispatchedScrollEvent() const { return mHasDispatchedScrollEvent; }
+
+ void ProcessElementTiming();
+
+ void AddImagesPendingRendering(ImagePendingRendering aImagePendingRendering) {
+ mImagesPendingRendering.AppendElement(aImagePendingRendering);
+ }
+
+ void StoreImageLCPEntry(Element* aElement, imgRequestProxy* aImgRequestProxy,
+ LargestContentfulPaint* aEntry);
+
+ already_AddRefed<LargestContentfulPaint> GetImageLCPEntry(
+ Element* aElement, imgRequestProxy* aImgRequestProxy);
+
+ bool UpdateLargestContentfulPaintSize(double aSize);
+ double GetLargestContentfulPaintSize() const {
+ return mLargestContentfulPaintSize;
+ }
+
+ nsTHashMap<nsRefPtrHashKey<Element>, nsRect>& GetTextFrameUnions() {
+ return mTextFrameUnions;
+ }
+
+ void FinalizeLCPEntriesForText();
+
+ void ClearGeneratedTempDataForLCP();
+
+ protected:
+ ~PerformanceMainThread();
+
+ void CreateNavigationTimingEntry();
+
+ void InsertUserEntry(PerformanceEntry* aEntry) override;
+
+ DOMHighResTimeStamp GetPerformanceTimingFromString(
+ const nsAString& aTimingName) override;
+
+ void DispatchBufferFullEvent() override;
+
+ RefPtr<PerformanceNavigationTiming> mDocEntry;
+ RefPtr<nsDOMNavigationTiming> mDOMTiming;
+ nsCOMPtr<nsITimedChannel> mChannel;
+ RefPtr<PerformanceTiming> mTiming;
+ RefPtr<PerformanceNavigation> mNavigation;
+ RefPtr<PerformancePaintTiming> mFCPTiming;
+ JS::Heap<JSObject*> mMozMemory;
+
+ nsTArray<RefPtr<PerformanceEventTiming>> mEventTimingEntries;
+ nsTArray<RefPtr<LargestContentfulPaint>> mLargestContentfulPaintEntries;
+
+ AutoCleanLinkedList<RefPtr<PerformanceEventTiming>>
+ mPendingEventTimingEntries;
+ bool mHasDispatchedInputEvent = false;
+ bool mHasDispatchedScrollEvent = false;
+
+ RefPtr<PerformanceEventTiming> mFirstInputEvent;
+ RefPtr<PerformanceEventTiming> mPendingPointerDown;
+
+ private:
+ void SetHasDispatchedInputEvent();
+
+ bool mHasQueuedRefreshdriverObserver = false;
+
+ RefPtr<class EventCounts> mEventCounts;
+ void IncEventCount(const nsAtom* aType);
+
+ PresShell* GetPresShell();
+
+ nsTArray<ImagePendingRendering> mImagesPendingRendering;
+
+ // The key is the pair of the element initiates the image loading
+ // and the imgRequestProxy of the image, and the value is
+ // the LCP entry for this image. When the image is
+ // completely loaded, we add it to mImageLCPEntryMap.
+ // Later, when the image is painted, we get the LCP entry from it
+ // to update the size and queue the entry if needed.
+ //
+ // When the initiating element is disconnected from the document,
+ // we keep the orphan entry because if the same memory address is
+ // reused by a different LCP candidate, it'll update
+ // mImageLCPEntryMap precedes before it tries to get the LCP entry.
+ ImageLCPEntryMap mImageLCPEntryMap;
+
+ // Keeps track of the rendered size of the largest contentful paint that
+ // we have processed so far.
+ double mLargestContentfulPaintSize = 0.0;
+
+ // When a text frame is painted, its area (relative to the
+ // containing block) is unioned with other text frames that
+ // belong to the same containing block.
+ // mTextFrameUnions's key is the containing block, and
+ // the value is the unioned area.
+ TextFrameUnions mTextFrameUnions;
+};
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback, ImageLCPEntryMap& aField,
+ const char* aName, uint32_t aFlags = 0) {
+ for (auto& entry : aField) {
+ RefPtr<LargestContentfulPaint>* lcpEntry = entry.GetModifiableData();
+ ImplCycleCollectionTraverse(aCallback, *lcpEntry, "ImageLCPEntryMap.mData",
+ aCallback.Flags());
+ }
+}
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback, TextFrameUnions& aField,
+ const char* aName, uint32_t aFlags = 0) {
+ for (auto& entry : aField) {
+ ImplCycleCollectionTraverse(
+ aCallback, entry, "TextFrameUnions's key (nsRefPtrHashKey<Element>)",
+ aFlags);
+ }
+}
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PerformanceMainThread_h
diff --git a/dom/performance/PerformanceMark.cpp b/dom/performance/PerformanceMark.cpp
new file mode 100644
index 0000000000..57258e2107
--- /dev/null
+++ b/dom/performance/PerformanceMark.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceMark.h"
+#include "MainThreadUtils.h"
+#include "nsContentUtils.h"
+#include "Performance.h"
+#include "mozilla/dom/MessagePortBinding.h"
+#include "mozilla/dom/PerformanceBinding.h"
+#include "mozilla/dom/PerformanceMarkBinding.h"
+
+using namespace mozilla::dom;
+
+PerformanceMark::PerformanceMark(nsISupports* aParent, const nsAString& aName,
+ DOMHighResTimeStamp aStartTime,
+ const JS::Handle<JS::Value>& aDetail,
+ DOMHighResTimeStamp aUnclampedStartTime)
+ : PerformanceEntry(aParent, aName, u"mark"_ns),
+ mStartTime(aStartTime),
+ mDetail(aDetail),
+ mUnclampedStartTime(aUnclampedStartTime) {
+ mozilla::HoldJSObjects(this);
+}
+
+already_AddRefed<PerformanceMark> PerformanceMark::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aMarkName,
+ const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) {
+ const nsCOMPtr<nsIGlobalObject> global =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ return PerformanceMark::Constructor(aGlobal.Context(), global, aMarkName,
+ aMarkOptions, aRv);
+}
+
+already_AddRefed<PerformanceMark> PerformanceMark::Constructor(
+ JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aMarkName,
+ const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) {
+ RefPtr<Performance> performance = Performance::Get(aCx, aGlobal);
+ if (!performance) {
+ // This is similar to the message that occurs when accessing `performance`
+ // from outside a valid global.
+ aRv.ThrowTypeError(
+ "can't access PerformanceMark constructor, performance is null");
+ return nullptr;
+ }
+
+ if (performance->IsGlobalObjectWindow() &&
+ performance->IsPerformanceTimingAttribute(aMarkName)) {
+ aRv.ThrowSyntaxError("markName cannot be a performance timing attribute");
+ return nullptr;
+ }
+
+ DOMHighResTimeStamp startTime = aMarkOptions.mStartTime.WasPassed()
+ ? aMarkOptions.mStartTime.Value()
+ : performance->Now();
+ // We need to get the unclamped start time to be able to add profiler markers
+ // with precise time/duration. This is not exposed to web and only used by the
+ // profiler.
+ // If a mStartTime is passed by the user, we will always have a clamped value.
+ DOMHighResTimeStamp unclampedStartTime = aMarkOptions.mStartTime.WasPassed()
+ ? startTime
+ : performance->NowUnclamped();
+ if (startTime < 0) {
+ aRv.ThrowTypeError("Expected startTime >= 0");
+ return nullptr;
+ }
+
+ JS::Rooted<JS::Value> detail(aCx);
+ if (aMarkOptions.mDetail.isNullOrUndefined()) {
+ detail.setNull();
+ } else {
+ StructuredSerializeOptions serializeOptions;
+ JS::Rooted<JS::Value> valueToClone(aCx, aMarkOptions.mDetail);
+ nsContentUtils::StructuredClone(aCx, aGlobal, valueToClone,
+ serializeOptions, &detail, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ }
+
+ return do_AddRef(new PerformanceMark(aGlobal, aMarkName, startTime, detail,
+ unclampedStartTime));
+}
+
+PerformanceMark::~PerformanceMark() { mozilla::DropJSObjects(this); }
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PerformanceMark)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PerformanceMark,
+ PerformanceEntry)
+ tmp->mDetail.setUndefined();
+ mozilla::DropJSObjects(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PerformanceMark,
+ PerformanceEntry)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceMark,
+ PerformanceEntry)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDetail)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(PerformanceMark,
+ PerformanceEntry)
+
+JSObject* PerformanceMark::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceMark_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void PerformanceMark::GetDetail(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval) {
+ // Return a copy so that this method always returns the value it is set to
+ // (i.e. it'll return the same value even if the caller assigns to it). Note
+ // that if detail is an object, its contents can be mutated and this is
+ // expected.
+ aRetval.set(mDetail);
+}
+
+size_t PerformanceMark::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
diff --git a/dom/performance/PerformanceMark.h b/dom/performance/PerformanceMark.h
new file mode 100644
index 0000000000..a67564442c
--- /dev/null
+++ b/dom/performance/PerformanceMark.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_performancemark_h___
+#define mozilla_dom_performancemark_h___
+
+#include "mozilla/dom/PerformanceEntry.h"
+#include "mozilla/ProfilerMarkers.h"
+
+namespace mozilla::dom {
+
+struct PerformanceMarkOptions;
+
+// http://www.w3.org/TR/user-timing/#performancemark
+class PerformanceMark final : public PerformanceEntry {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(PerformanceMark,
+ PerformanceEntry);
+
+ private:
+ PerformanceMark(nsISupports* aParent, const nsAString& aName,
+ DOMHighResTimeStamp aStartTime,
+ const JS::Handle<JS::Value>& aDetail,
+ DOMHighResTimeStamp aUnclampedStartTime);
+
+ public:
+ static already_AddRefed<PerformanceMark> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aMarkName,
+ const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv);
+
+ static already_AddRefed<PerformanceMark> Constructor(
+ JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aMarkName,
+ const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual DOMHighResTimeStamp StartTime() const override { return mStartTime; }
+
+ virtual DOMHighResTimeStamp UnclampedStartTime() const override {
+ MOZ_ASSERT(profiler_thread_is_being_profiled_for_markers(),
+ "This should only be called when the Gecko Profiler is active.");
+ return mUnclampedStartTime;
+ }
+
+ void GetDetail(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
+
+ size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const override;
+
+ protected:
+ virtual ~PerformanceMark();
+ DOMHighResTimeStamp mStartTime;
+
+ private:
+ JS::Heap<JS::Value> mDetail;
+ // This is used by the Gecko Profiler only to be able to add precise markers.
+ // It's not exposed to JS
+ DOMHighResTimeStamp mUnclampedStartTime;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_performancemark_h___ */
diff --git a/dom/performance/PerformanceMeasure.cpp b/dom/performance/PerformanceMeasure.cpp
new file mode 100644
index 0000000000..d276162b42
--- /dev/null
+++ b/dom/performance/PerformanceMeasure.cpp
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceMeasure.h"
+#include "MainThreadUtils.h"
+#include "mozilla/dom/PerformanceMeasureBinding.h"
+
+using namespace mozilla::dom;
+
+PerformanceMeasure::PerformanceMeasure(nsISupports* aParent,
+ const nsAString& aName,
+ DOMHighResTimeStamp aStartTime,
+ DOMHighResTimeStamp aEndTime,
+ const JS::Handle<JS::Value>& aDetail)
+ : PerformanceEntry(aParent, aName, u"measure"_ns),
+ mStartTime(aStartTime),
+ mDuration(aEndTime - aStartTime),
+ mDetail(aDetail) {
+ mozilla::HoldJSObjects(this);
+}
+
+PerformanceMeasure::~PerformanceMeasure() { mozilla::DropJSObjects(this); }
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PerformanceMeasure)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PerformanceMeasure,
+ PerformanceEntry)
+ tmp->mDetail.setUndefined();
+ mozilla::DropJSObjects(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PerformanceMeasure,
+ PerformanceEntry)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceMeasure,
+ PerformanceEntry)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mDetail)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(PerformanceMeasure,
+ PerformanceEntry)
+
+JSObject* PerformanceMeasure::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceMeasure_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void PerformanceMeasure::GetDetail(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aRetval) {
+ // Return a copy so that this method always returns the value it is set to
+ // (i.e. it'll return the same value even if the caller assigns to it). Note
+ // that if detail is an object, its contents can be mutated and this is
+ // expected.
+ aRetval.set(mDetail);
+}
+
+size_t PerformanceMeasure::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
diff --git a/dom/performance/PerformanceMeasure.h b/dom/performance/PerformanceMeasure.h
new file mode 100644
index 0000000000..7ac9ceb16d
--- /dev/null
+++ b/dom/performance/PerformanceMeasure.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_performancemeasure_h___
+#define mozilla_dom_performancemeasure_h___
+
+#include "mozilla/dom/PerformanceEntry.h"
+
+namespace mozilla::dom {
+
+// http://www.w3.org/TR/user-timing/#performancemeasure
+class PerformanceMeasure final : public PerformanceEntry {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(PerformanceMeasure,
+ PerformanceEntry);
+
+ PerformanceMeasure(nsISupports* aParent, const nsAString& aName,
+ DOMHighResTimeStamp aStartTime,
+ DOMHighResTimeStamp aEndTime,
+ const JS::Handle<JS::Value>& aDetail);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual DOMHighResTimeStamp StartTime() const override { return mStartTime; }
+
+ virtual DOMHighResTimeStamp Duration() const override { return mDuration; }
+
+ void GetDetail(JSContext* aCx, JS::MutableHandle<JS::Value> aRetval);
+
+ size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const override;
+
+ protected:
+ virtual ~PerformanceMeasure();
+ DOMHighResTimeStamp mStartTime;
+ DOMHighResTimeStamp mDuration;
+
+ private:
+ JS::Heap<JS::Value> mDetail;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_performancemeasure_h___ */
diff --git a/dom/performance/PerformanceNavigation.cpp b/dom/performance/PerformanceNavigation.cpp
new file mode 100644
index 0000000000..bbfdd4a982
--- /dev/null
+++ b/dom/performance/PerformanceNavigation.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceNavigation.h"
+#include "PerformanceTiming.h"
+#include "mozilla/dom/PerformanceNavigationBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PerformanceNavigation, mPerformance)
+
+PerformanceNavigation::PerformanceNavigation(Performance* aPerformance)
+ : mPerformance(aPerformance) {
+ MOZ_ASSERT(aPerformance, "Parent performance object should be provided");
+}
+
+PerformanceNavigation::~PerformanceNavigation() = default;
+
+JSObject* PerformanceNavigation::WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceNavigation_Binding::Wrap(cx, this, aGivenProto);
+}
+
+uint16_t PerformanceNavigation::RedirectCount() const {
+ return GetPerformanceTiming()->Data()->GetRedirectCount();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceNavigation.h b/dom/performance/PerformanceNavigation.h
new file mode 100644
index 0000000000..2f6e21fe65
--- /dev/null
+++ b/dom/performance/PerformanceNavigation.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceNavigation_h
+#define mozilla_dom_PerformanceNavigation_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Performance.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+
+// Script "performance.navigation" object
+class PerformanceNavigation final : public nsWrapperCache {
+ public:
+ explicit PerformanceNavigation(Performance* aPerformance);
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PerformanceNavigation)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(PerformanceNavigation)
+
+ nsDOMNavigationTiming* GetDOMTiming() const {
+ return mPerformance->GetDOMTiming();
+ }
+
+ PerformanceTiming* GetPerformanceTiming() const {
+ return mPerformance->Timing();
+ }
+
+ Performance* GetParentObject() const { return mPerformance; }
+
+ virtual JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // PerformanceNavigation WebIDL methods
+ uint16_t Type() const { return GetDOMTiming()->GetType(); }
+
+ uint16_t RedirectCount() const;
+
+ private:
+ ~PerformanceNavigation();
+ RefPtr<Performance> mPerformance;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PerformanceNavigation_h
diff --git a/dom/performance/PerformanceNavigationTiming.cpp b/dom/performance/PerformanceNavigationTiming.cpp
new file mode 100644
index 0000000000..6bd1cc1fbe
--- /dev/null
+++ b/dom/performance/PerformanceNavigationTiming.cpp
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "mozilla/dom/PerformanceNavigationTiming.h"
+#include "mozilla/dom/PerformanceNavigationTimingBinding.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_privacy.h"
+
+using namespace mozilla::dom;
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceNavigationTiming)
+NS_INTERFACE_MAP_END_INHERITING(PerformanceResourceTiming)
+
+NS_IMPL_ADDREF_INHERITED(PerformanceNavigationTiming, PerformanceResourceTiming)
+NS_IMPL_RELEASE_INHERITED(PerformanceNavigationTiming,
+ PerformanceResourceTiming)
+
+JSObject* PerformanceNavigationTiming::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceNavigationTiming_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+#define REDUCE_TIME_PRECISION \
+ return nsRFPService::ReduceTimePrecisionAsMSecs( \
+ rawValue, mPerformance->GetRandomTimelineSeed(), \
+ mPerformance->GetRTPCallerType())
+
+DOMHighResTimeStamp PerformanceNavigationTiming::UnloadEventStart() const {
+ DOMHighResTimeStamp rawValue = 0;
+ /*
+ * Per Navigation Timing Level 2, the value is 0 if
+ * a. there is no previous document or
+ * b. the same-origin-check fails.
+ *
+ * The same-origin-check is defined as:
+ * 1. If the previous document exists and its origin is not same
+ * origin as the current document's origin, return "fail".
+ * 2. Let request be the current document's request.
+ * 3. If request's redirect count is not zero, and all of request's
+ * HTTP redirects have the same origin as the current document,
+ * return "pass".
+ * 4. Otherwise, return "fail".
+ */
+ if (mTimingData->AllRedirectsSameOrigin()) { // same-origin-check:2/3
+ /*
+ * GetUnloadEventStartHighRes returns 0 if
+ * 1. there is no previous document (a, above) or
+ * 2. the current URI does not have the same origin as
+ * the previous document's URI. (same-origin-check:1)
+ */
+ rawValue = mPerformance->GetDOMTiming()->GetUnloadEventStartHighRes();
+ }
+
+ REDUCE_TIME_PRECISION;
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::UnloadEventEnd() const {
+ DOMHighResTimeStamp rawValue = 0;
+
+ // See comments in PerformanceNavigationTiming::UnloadEventEnd().
+ if (mTimingData->AllRedirectsSameOrigin()) {
+ rawValue = mPerformance->GetDOMTiming()->GetUnloadEventEndHighRes();
+ }
+
+ REDUCE_TIME_PRECISION;
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::DomInteractive() const {
+ DOMHighResTimeStamp rawValue =
+ mPerformance->GetDOMTiming()->GetDomInteractiveHighRes();
+
+ REDUCE_TIME_PRECISION;
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::DomContentLoadedEventStart()
+ const {
+ DOMHighResTimeStamp rawValue =
+ mPerformance->GetDOMTiming()->GetDomContentLoadedEventStartHighRes();
+
+ REDUCE_TIME_PRECISION;
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::DomContentLoadedEventEnd()
+ const {
+ DOMHighResTimeStamp rawValue =
+ mPerformance->GetDOMTiming()->GetDomContentLoadedEventEndHighRes();
+
+ REDUCE_TIME_PRECISION;
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::DomComplete() const {
+ DOMHighResTimeStamp rawValue =
+ mPerformance->GetDOMTiming()->GetDomCompleteHighRes();
+
+ REDUCE_TIME_PRECISION;
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::LoadEventStart() const {
+ DOMHighResTimeStamp rawValue =
+ mPerformance->GetDOMTiming()->GetLoadEventStartHighRes();
+
+ REDUCE_TIME_PRECISION;
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::LoadEventEnd() const {
+ DOMHighResTimeStamp rawValue =
+ mPerformance->GetDOMTiming()->GetLoadEventEndHighRes();
+
+ REDUCE_TIME_PRECISION;
+}
+
+NavigationType PerformanceNavigationTiming::Type() const {
+ switch (mPerformance->GetDOMTiming()->GetType()) {
+ case nsDOMNavigationTiming::TYPE_NAVIGATE:
+ return NavigationType::Navigate;
+ break;
+ case nsDOMNavigationTiming::TYPE_RELOAD:
+ return NavigationType::Reload;
+ break;
+ case nsDOMNavigationTiming::TYPE_BACK_FORWARD:
+ return NavigationType::Back_forward;
+ break;
+ default:
+ // The type is TYPE_RESERVED or some other value that was later added.
+ // We fallback to the default of Navigate.
+ return NavigationType::Navigate;
+ }
+}
+
+uint16_t PerformanceNavigationTiming::RedirectCount() const {
+ return mTimingData->GetRedirectCount();
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::RedirectStart(
+ nsIPrincipal& aSubjectPrincipal) const {
+ return PerformanceResourceTiming::RedirectStart(
+ aSubjectPrincipal, true /* aEnsureSameOriginAndIgnoreTAO */);
+}
+
+DOMHighResTimeStamp PerformanceNavigationTiming::RedirectEnd(
+ nsIPrincipal& aSubjectPrincipal) const {
+ return PerformanceResourceTiming::RedirectEnd(
+ aSubjectPrincipal, true /* aEnsureSameOriginAndIgnoreTAO */);
+}
+
+void PerformanceNavigationTiming::UpdatePropertiesFromHttpChannel(
+ nsIHttpChannel* aHttpChannel, nsITimedChannel* aChannel) {
+ mTimingData->SetPropertiesFromHttpChannel(aHttpChannel, aChannel);
+}
+
+bool PerformanceNavigationTiming::Enabled(JSContext* aCx, JSObject* aGlobal) {
+ return StaticPrefs::dom_enable_performance_navigation_timing();
+}
diff --git a/dom/performance/PerformanceNavigationTiming.h b/dom/performance/PerformanceNavigationTiming.h
new file mode 100644
index 0000000000..2a10b34581
--- /dev/null
+++ b/dom/performance/PerformanceNavigationTiming.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceNavigationTiming_h___
+#define mozilla_dom_PerformanceNavigationTiming_h___
+
+#include <stdint.h>
+#include <utility>
+#include "js/RootingAPI.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/PerformanceNavigationTimingBinding.h"
+#include "mozilla/dom/PerformanceResourceTiming.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsISupports.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+#include "nsTLiteralString.h"
+
+class JSObject;
+class nsIHttpChannel;
+class nsITimedChannel;
+struct JSContext;
+
+namespace mozilla::dom {
+
+class Performance;
+class PerformanceTimingData;
+
+// https://www.w3.org/TR/navigation-timing-2/#sec-PerformanceNavigationTiming
+class PerformanceNavigationTiming final : public PerformanceResourceTiming {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ // Note that aPerformanceTiming must be initalized with zeroTime = 0
+ // so that timestamps are relative to startTime, as opposed to the
+ // performance.timing object for which timestamps are absolute and has a
+ // zeroTime initialized to navigationStart
+ // aPerformanceTiming and aPerformance must be non-null.
+ PerformanceNavigationTiming(
+ UniquePtr<PerformanceTimingData>&& aPerformanceTiming,
+ Performance* aPerformance, const nsAString& aName)
+ : PerformanceResourceTiming(std::move(aPerformanceTiming), aPerformance,
+ aName) {
+ SetEntryType(u"navigation"_ns);
+ SetInitiatorType(u"navigation"_ns);
+ }
+
+ DOMHighResTimeStamp Duration() const override {
+ return LoadEventEnd() - StartTime();
+ }
+
+ DOMHighResTimeStamp StartTime() const override { return 0; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ DOMHighResTimeStamp UnloadEventStart() const;
+ DOMHighResTimeStamp UnloadEventEnd() const;
+
+ DOMHighResTimeStamp DomInteractive() const;
+ DOMHighResTimeStamp DomContentLoadedEventStart() const;
+ DOMHighResTimeStamp DomContentLoadedEventEnd() const;
+ DOMHighResTimeStamp DomComplete() const;
+ DOMHighResTimeStamp LoadEventStart() const;
+ DOMHighResTimeStamp LoadEventEnd() const;
+
+ DOMHighResTimeStamp RedirectStart(
+ nsIPrincipal& aSubjectPrincipal) const override;
+ DOMHighResTimeStamp RedirectEnd(
+ nsIPrincipal& aSubjectPrincipal) const override;
+
+ NavigationType Type() const;
+ uint16_t RedirectCount() const;
+
+ void UpdatePropertiesFromHttpChannel(nsIHttpChannel* aHttpChannel,
+ nsITimedChannel* aChannel);
+
+ /*
+ * For use with the WebIDL Func attribute to determine whether
+ * window.PerformanceNavigationTiming is exposed.
+ */
+ static bool Enabled(JSContext* aCx, JSObject* aGlobal);
+
+ private:
+ ~PerformanceNavigationTiming() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PerformanceNavigationTiming_h___
diff --git a/dom/performance/PerformanceObserver.cpp b/dom/performance/PerformanceObserver.cpp
new file mode 100644
index 0000000000..d5d1725c57
--- /dev/null
+++ b/dom/performance/PerformanceObserver.cpp
@@ -0,0 +1,384 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceObserver.h"
+
+#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/PerformanceBinding.h"
+#include "mozilla/dom/PerformanceEntryBinding.h"
+#include "mozilla/dom/PerformanceObserverBinding.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsIScriptError.h"
+#include "nsPIDOMWindow.h"
+#include "nsQueryObject.h"
+#include "nsString.h"
+#include "PerformanceEntry.h"
+#include "LargestContentfulPaint.h"
+#include "PerformanceObserverEntryList.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PerformanceObserver)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PerformanceObserver)
+ tmp->Disconnect();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallback)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPerformance)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mQueuedEntries)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PerformanceObserver)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallback)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPerformance)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mQueuedEntries)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PerformanceObserver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PerformanceObserver)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceObserver)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+const char UnsupportedEntryTypesIgnoredMsgId[] = "UnsupportedEntryTypesIgnored";
+const char AllEntryTypesIgnoredMsgId[] = "AllEntryTypesIgnored";
+
+PerformanceObserver::PerformanceObserver(nsPIDOMWindowInner* aOwner,
+ PerformanceObserverCallback& aCb)
+ : mOwner(aOwner),
+ mCallback(&aCb),
+ mObserverType(ObserverTypeUndefined),
+ mConnected(false) {
+ MOZ_ASSERT(mOwner);
+ mPerformance = aOwner->GetPerformance();
+}
+
+PerformanceObserver::PerformanceObserver(WorkerPrivate* aWorkerPrivate,
+ PerformanceObserverCallback& aCb)
+ : mCallback(&aCb), mObserverType(ObserverTypeUndefined), mConnected(false) {
+ MOZ_ASSERT(aWorkerPrivate);
+ mPerformance = aWorkerPrivate->GlobalScope()->GetPerformance();
+}
+
+PerformanceObserver::~PerformanceObserver() {
+ Disconnect();
+ MOZ_ASSERT(!mConnected);
+}
+
+// static
+already_AddRefed<PerformanceObserver> PerformanceObserver::Constructor(
+ const GlobalObject& aGlobal, PerformanceObserverCallback& aCb,
+ ErrorResult& aRv) {
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!ownerWindow) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<PerformanceObserver> observer =
+ new PerformanceObserver(ownerWindow, aCb);
+ return observer.forget();
+ }
+
+ JSContext* cx = aGlobal.Context();
+ WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx);
+ MOZ_ASSERT(workerPrivate);
+
+ RefPtr<PerformanceObserver> observer =
+ new PerformanceObserver(workerPrivate, aCb);
+ return observer.forget();
+}
+
+JSObject* PerformanceObserver::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceObserver_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void PerformanceObserver::Notify() {
+ if (mQueuedEntries.IsEmpty()) {
+ return;
+ }
+ RefPtr<PerformanceObserverEntryList> list =
+ new PerformanceObserverEntryList(this, mQueuedEntries);
+
+ mQueuedEntries.Clear();
+
+ ErrorResult rv;
+ RefPtr<PerformanceObserverCallback> callback(mCallback);
+ callback->Call(this, *list, *this, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ }
+}
+
+void PerformanceObserver::QueueEntry(PerformanceEntry* aEntry) {
+ MOZ_ASSERT(aEntry);
+ MOZ_ASSERT(ObservesTypeOfEntry(aEntry));
+
+ mQueuedEntries.AppendElement(aEntry);
+}
+
+static constexpr nsLiteralString kValidEventTimingNames[2] = {
+ u"event"_ns, u"first-input"_ns};
+
+/*
+ * Keep this list in alphabetical order.
+ * https://w3c.github.io/performance-timeline/#supportedentrytypes-attribute
+ */
+static constexpr nsLiteralString kValidTypeNames[5] = {
+ u"mark"_ns, u"measure"_ns, u"navigation"_ns, u"paint"_ns, u"resource"_ns,
+};
+
+void PerformanceObserver::ReportUnsupportedTypesErrorToConsole(
+ bool aIsMainThread, const char* msgId, const nsString& aInvalidTypes) {
+ if (!aIsMainThread) {
+ nsTArray<nsString> params;
+ params.AppendElement(aInvalidTypes);
+ WorkerPrivate::ReportErrorToConsole(msgId, params);
+ } else {
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow = do_QueryInterface(mOwner);
+ Document* document = ownerWindow->GetExtantDoc();
+ AutoTArray<nsString, 1> params = {aInvalidTypes};
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns,
+ document, nsContentUtils::eDOM_PROPERTIES,
+ msgId, params);
+ }
+}
+
+void PerformanceObserver::Observe(const PerformanceObserverInit& aOptions,
+ ErrorResult& aRv) {
+ const Optional<Sequence<nsString>>& maybeEntryTypes = aOptions.mEntryTypes;
+ const Optional<nsString>& maybeType = aOptions.mType;
+ const Optional<bool>& maybeBuffered = aOptions.mBuffered;
+
+ if (!mPerformance) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return;
+ }
+
+ if (!maybeEntryTypes.WasPassed() && !maybeType.WasPassed()) {
+ /* Per spec (3.3.1.2), this should be a syntax error. */
+ aRv.ThrowTypeError("Can't call observe without `type` or `entryTypes`");
+ return;
+ }
+
+ if (maybeEntryTypes.WasPassed() &&
+ (maybeType.WasPassed() || maybeBuffered.WasPassed())) {
+ /* Per spec (3.3.1.3), this, too, should be a syntax error. */
+ aRv.ThrowTypeError("Can't call observe with both `type` and `entryTypes`");
+ return;
+ }
+
+ /* 3.3.1.4.1 */
+ if (mObserverType == ObserverTypeUndefined) {
+ if (maybeEntryTypes.WasPassed()) {
+ mObserverType = ObserverTypeMultiple;
+ } else {
+ mObserverType = ObserverTypeSingle;
+ }
+ }
+
+ /* 3.3.1.4.2 */
+ if (mObserverType == ObserverTypeSingle && maybeEntryTypes.WasPassed()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_MODIFICATION_ERR);
+ return;
+ }
+ /* 3.3.1.4.3 */
+ if (mObserverType == ObserverTypeMultiple && maybeType.WasPassed()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_MODIFICATION_ERR);
+ return;
+ }
+
+ bool needQueueNotificationObserverTask = false;
+ /* 3.3.1.5 */
+ if (mObserverType == ObserverTypeMultiple) {
+ const Sequence<nsString>& entryTypes = maybeEntryTypes.Value();
+
+ if (entryTypes.IsEmpty()) {
+ return;
+ }
+
+ /* 3.3.1.5.2 */
+ nsTArray<nsString> validEntryTypes;
+
+ if (StaticPrefs::dom_enable_event_timing()) {
+ for (const nsLiteralString& name : kValidEventTimingNames) {
+ if (entryTypes.Contains(name) && !validEntryTypes.Contains(name)) {
+ validEntryTypes.AppendElement(name);
+ }
+ }
+ }
+ if (StaticPrefs::dom_enable_largest_contentful_paint()) {
+ if (entryTypes.Contains(kLargestContentfulPaintName) &&
+ !validEntryTypes.Contains(kLargestContentfulPaintName)) {
+ validEntryTypes.AppendElement(kLargestContentfulPaintName);
+ }
+ }
+ for (const nsLiteralString& name : kValidTypeNames) {
+ if (entryTypes.Contains(name) && !validEntryTypes.Contains(name)) {
+ validEntryTypes.AppendElement(name);
+ }
+ }
+
+ nsAutoString invalidTypesJoined;
+ bool addComma = false;
+ for (const auto& type : entryTypes) {
+ if (!validEntryTypes.Contains<nsString>(type)) {
+ if (addComma) {
+ invalidTypesJoined.AppendLiteral(", ");
+ }
+ addComma = true;
+ invalidTypesJoined.Append(type);
+ }
+ }
+
+ if (!invalidTypesJoined.IsEmpty()) {
+ ReportUnsupportedTypesErrorToConsole(NS_IsMainThread(),
+ UnsupportedEntryTypesIgnoredMsgId,
+ invalidTypesJoined);
+ }
+
+ /* 3.3.1.5.3 */
+ if (validEntryTypes.IsEmpty()) {
+ nsString errorString;
+ ReportUnsupportedTypesErrorToConsole(
+ NS_IsMainThread(), AllEntryTypesIgnoredMsgId, errorString);
+ return;
+ }
+
+ /*
+ * Registered or not, we clear out the list of options, and start fresh
+ * with the one that we are using here. (3.3.1.5.4,5)
+ */
+ mOptions.Clear();
+ mOptions.AppendElement(aOptions);
+
+ } else {
+ MOZ_ASSERT(mObserverType == ObserverTypeSingle);
+ bool typeValid = false;
+ nsString type = maybeType.Value();
+
+ /* 3.3.1.6.2 */
+ if (StaticPrefs::dom_enable_event_timing()) {
+ for (const nsLiteralString& name : kValidEventTimingNames) {
+ if (type == name) {
+ typeValid = true;
+ break;
+ }
+ }
+ }
+ for (const nsLiteralString& name : kValidTypeNames) {
+ if (type == name) {
+ typeValid = true;
+ break;
+ }
+ }
+
+ if (StaticPrefs::dom_enable_largest_contentful_paint()) {
+ if (type == kLargestContentfulPaintName) {
+ typeValid = true;
+ }
+ }
+
+ if (!typeValid) {
+ ReportUnsupportedTypesErrorToConsole(
+ NS_IsMainThread(), UnsupportedEntryTypesIgnoredMsgId, type);
+ return;
+ }
+
+ /* 3.3.1.6.4, 3.3.1.6.4 */
+ bool didUpdateOptionsList = false;
+ nsTArray<PerformanceObserverInit> updatedOptionsList;
+ for (auto& option : mOptions) {
+ if (option.mType.WasPassed() && option.mType.Value() == type) {
+ updatedOptionsList.AppendElement(aOptions);
+ didUpdateOptionsList = true;
+ } else {
+ updatedOptionsList.AppendElement(option);
+ }
+ }
+ if (!didUpdateOptionsList) {
+ updatedOptionsList.AppendElement(aOptions);
+ }
+ mOptions = std::move(updatedOptionsList);
+
+ /* 3.3.1.6.5 */
+ if (maybeBuffered.WasPassed() && maybeBuffered.Value()) {
+ nsTArray<RefPtr<PerformanceEntry>> existingEntries;
+ mPerformance->GetEntriesByTypeForObserver(type, existingEntries);
+ if (!existingEntries.IsEmpty()) {
+ mQueuedEntries.AppendElements(existingEntries);
+ needQueueNotificationObserverTask = true;
+ }
+ }
+ }
+ /* Add ourselves to the list of registered performance
+ * observers, if necessary. (3.3.1.5.4,5; 3.3.1.6.4)
+ */
+ mPerformance->AddObserver(this);
+
+ if (needQueueNotificationObserverTask) {
+ mPerformance->QueueNotificationObserversTask();
+ }
+ mConnected = true;
+}
+
+void PerformanceObserver::GetSupportedEntryTypes(
+ const GlobalObject& aGlobal, JS::MutableHandle<JSObject*> aObject) {
+ nsTArray<nsString> validTypes;
+ JS::Rooted<JS::Value> val(aGlobal.Context());
+
+ if (StaticPrefs::dom_enable_event_timing()) {
+ for (const nsLiteralString& name : kValidEventTimingNames) {
+ validTypes.AppendElement(name);
+ }
+ }
+
+ if (StaticPrefs::dom_enable_largest_contentful_paint()) {
+ validTypes.AppendElement(u"largest-contentful-paint"_ns);
+ }
+ for (const nsLiteralString& name : kValidTypeNames) {
+ validTypes.AppendElement(name);
+ }
+
+ if (!ToJSValue(aGlobal.Context(), validTypes, &val)) {
+ /*
+ * If this conversion fails, we don't set a result.
+ * The spec does not allow us to throw an exception.
+ */
+ return;
+ }
+ aObject.set(&val.toObject());
+}
+
+bool PerformanceObserver::ObservesTypeOfEntry(PerformanceEntry* aEntry) {
+ for (auto& option : mOptions) {
+ if (aEntry->ShouldAddEntryToObserverBuffer(option)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void PerformanceObserver::Disconnect() {
+ if (mConnected) {
+ MOZ_ASSERT(mPerformance);
+ mPerformance->RemoveObserver(this);
+ mOptions.Clear();
+ mConnected = false;
+ }
+}
+
+void PerformanceObserver::TakeRecords(
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ MOZ_ASSERT(aRetval.IsEmpty());
+ aRetval = std::move(mQueuedEntries);
+}
diff --git a/dom/performance/PerformanceObserver.h b/dom/performance/PerformanceObserver.h
new file mode 100644
index 0000000000..7b50eb8b60
--- /dev/null
+++ b/dom/performance/PerformanceObserver.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceObserver_h__
+#define mozilla_dom_PerformanceObserver_h__
+
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "mozilla/dom/PerformanceObserverBinding.h"
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class GlobalObject;
+class Performance;
+class PerformanceEntry;
+class PerformanceObserverCallback;
+class WorkerPrivate;
+
+class PerformanceObserver final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PerformanceObserver)
+
+ static already_AddRefed<PerformanceObserver> Constructor(
+ const GlobalObject& aGlobal, PerformanceObserverCallback& aCb,
+ ErrorResult& aRv);
+
+ PerformanceObserver(nsPIDOMWindowInner* aOwner,
+ PerformanceObserverCallback& aCb);
+
+ PerformanceObserver(WorkerPrivate* aWorkerPrivate,
+ PerformanceObserverCallback& aCb);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject() const { return mOwner; }
+
+ void Observe(const PerformanceObserverInit& aOptions, ErrorResult& aRv);
+ static void GetSupportedEntryTypes(const GlobalObject& aGlobal,
+ JS::MutableHandle<JSObject*> aObject);
+
+ void Disconnect();
+
+ void TakeRecords(nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+
+ MOZ_CAN_RUN_SCRIPT void Notify();
+ void QueueEntry(PerformanceEntry* aEntry);
+
+ bool ObservesTypeOfEntry(PerformanceEntry* aEntry);
+
+ private:
+ void ReportUnsupportedTypesErrorToConsole(bool aIsMainThread,
+ const char* msgId,
+ const nsString& aInvalidTypes);
+ ~PerformanceObserver();
+
+ nsCOMPtr<nsISupports> mOwner;
+ RefPtr<PerformanceObserverCallback> mCallback;
+ RefPtr<Performance> mPerformance;
+ nsTArray<nsString> mEntryTypes;
+ nsTArray<PerformanceObserverInit> mOptions;
+ enum {
+ ObserverTypeUndefined,
+ ObserverTypeSingle,
+ ObserverTypeMultiple,
+ } mObserverType;
+ /*
+ * This is also known as registered, in the spec.
+ */
+ bool mConnected;
+ nsTArray<RefPtr<PerformanceEntry>> mQueuedEntries;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/performance/PerformanceObserverEntryList.cpp b/dom/performance/PerformanceObserverEntryList.cpp
new file mode 100644
index 0000000000..af4d867cd7
--- /dev/null
+++ b/dom/performance/PerformanceObserverEntryList.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceObserverEntryList.h"
+
+#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/PerformanceObserverEntryListBinding.h"
+#include "nsString.h"
+#include "PerformanceResourceTiming.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PerformanceObserverEntryList, mOwner,
+ mEntries)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PerformanceObserverEntryList)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PerformanceObserverEntryList)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceObserverEntryList)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+PerformanceObserverEntryList::~PerformanceObserverEntryList() = default;
+
+JSObject* PerformanceObserverEntryList::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceObserverEntryList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void PerformanceObserverEntryList::GetEntries(
+ const PerformanceEntryFilterOptions& aFilter,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ aRetval.Clear();
+ RefPtr<nsAtom> name =
+ aFilter.mName.WasPassed() ? NS_Atomize(aFilter.mName.Value()) : nullptr;
+ RefPtr<nsAtom> entryType = aFilter.mEntryType.WasPassed()
+ ? NS_Atomize(aFilter.mEntryType.Value())
+ : nullptr;
+ for (const RefPtr<PerformanceEntry>& entry : mEntries) {
+ if (aFilter.mInitiatorType.WasPassed()) {
+ const PerformanceResourceTiming* resourceEntry =
+ entry->ToResourceTiming();
+ if (!resourceEntry) {
+ continue;
+ }
+ nsAutoString initiatorType;
+ resourceEntry->GetInitiatorType(initiatorType);
+ if (!initiatorType.Equals(aFilter.mInitiatorType.Value())) {
+ continue;
+ }
+ }
+ if (name && entry->GetName() != name) {
+ continue;
+ }
+ if (entryType && entry->GetEntryType() != entryType) {
+ continue;
+ }
+
+ aRetval.AppendElement(entry);
+ }
+ aRetval.Sort(PerformanceEntryComparator());
+}
+
+void PerformanceObserverEntryList::GetEntriesByType(
+ const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ aRetval.Clear();
+ RefPtr<nsAtom> entryType = NS_Atomize(aEntryType);
+ for (const RefPtr<PerformanceEntry>& entry : mEntries) {
+ if (entry->GetEntryType() == entryType) {
+ aRetval.AppendElement(entry);
+ }
+ }
+ aRetval.Sort(PerformanceEntryComparator());
+}
+
+void PerformanceObserverEntryList::GetEntriesByName(
+ const nsAString& aName, const Optional<nsAString>& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
+ aRetval.Clear();
+ RefPtr<nsAtom> name = NS_Atomize(aName);
+ RefPtr<nsAtom> entryType =
+ aEntryType.WasPassed() ? NS_Atomize(aEntryType.Value()) : nullptr;
+ for (const RefPtr<PerformanceEntry>& entry : mEntries) {
+ if (entry->GetName() != name) {
+ continue;
+ }
+
+ if (entryType && entry->GetEntryType() != entryType) {
+ continue;
+ }
+
+ aRetval.AppendElement(entry);
+ }
+ aRetval.Sort(PerformanceEntryComparator());
+}
diff --git a/dom/performance/PerformanceObserverEntryList.h b/dom/performance/PerformanceObserverEntryList.h
new file mode 100644
index 0000000000..3a353060da
--- /dev/null
+++ b/dom/performance/PerformanceObserverEntryList.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceObserverEntryList_h__
+#define mozilla_dom_PerformanceObserverEntryList_h__
+
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+#include "mozilla/dom/PerformanceEntryBinding.h"
+
+namespace mozilla::dom {
+
+struct PerformanceEntryFilterOptions;
+class PerformanceEntry;
+template <typename T>
+class Optional;
+
+class PerformanceObserverEntryList final : public nsISupports,
+ public nsWrapperCache {
+ ~PerformanceObserverEntryList();
+
+ public:
+ PerformanceObserverEntryList(
+ nsISupports* aOwner, const nsTArray<RefPtr<PerformanceEntry>>& aEntries)
+ : mOwner(aOwner), mEntries(aEntries.Clone()) {}
+
+ nsISupports* GetParentObject() const { return mOwner; }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PerformanceObserverEntryList)
+
+ void GetEntries(const PerformanceEntryFilterOptions& aFilter,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+ void GetEntriesByType(const nsAString& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+ void GetEntriesByName(const nsAString& aName,
+ const Optional<nsAString>& aEntryType,
+ nsTArray<RefPtr<PerformanceEntry>>& aRetval);
+
+ private:
+ nsCOMPtr<nsISupports> mOwner;
+ nsTArray<RefPtr<PerformanceEntry>> mEntries;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/performance/PerformancePaintTiming.cpp b/dom/performance/PerformancePaintTiming.cpp
new file mode 100644
index 0000000000..83eed06565
--- /dev/null
+++ b/dom/performance/PerformancePaintTiming.cpp
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformancePaintTiming.h"
+#include "Performance.h"
+#include "MainThreadUtils.h"
+#include "mozilla/dom/PerformanceMeasureBinding.h"
+#include "nsRFPService.h"
+
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PerformancePaintTiming, PerformanceEntry,
+ mPerformance)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformancePaintTiming)
+NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry)
+
+NS_IMPL_ADDREF_INHERITED(PerformancePaintTiming, PerformanceEntry)
+NS_IMPL_RELEASE_INHERITED(PerformancePaintTiming, PerformanceEntry)
+
+PerformancePaintTiming::PerformancePaintTiming(Performance* aPerformance,
+ const nsAString& aName,
+ const TimeStamp& aStartTime)
+ : PerformanceEntry(aPerformance->GetParentObject(), aName, u"paint"_ns),
+ mPerformance(aPerformance),
+ mRawStartTime(aStartTime) {}
+
+PerformancePaintTiming::~PerformancePaintTiming() = default;
+
+JSObject* PerformancePaintTiming::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return PerformancePaintTiming_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+DOMHighResTimeStamp PerformancePaintTiming::StartTime() const {
+ if (mCachedStartTime.isNothing()) {
+ DOMHighResTimeStamp rawValue =
+ mPerformance->GetDOMTiming()->TimeStampToDOMHighRes(mRawStartTime);
+ mCachedStartTime.emplace(nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType()));
+ }
+ return mCachedStartTime.value();
+}
+
+size_t PerformancePaintTiming::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
diff --git a/dom/performance/PerformancePaintTiming.h b/dom/performance/PerformancePaintTiming.h
new file mode 100644
index 0000000000..395866dbc2
--- /dev/null
+++ b/dom/performance/PerformancePaintTiming.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformancePaintTiming_h___
+#define mozilla_dom_PerformancePaintTiming_h___
+
+#include "mozilla/dom/PerformanceEntry.h"
+#include "mozilla/dom/PerformancePaintTimingBinding.h"
+
+namespace mozilla::dom {
+
+class Performance;
+
+// https://w3c.github.io/paint-timing/#sec-PerformancePaintTiming
+// Unlike timeToContentfulPaint, this timing is generated during
+// displaylist building, when a frame is contentful, we collect
+// the timestamp. Whereas, timeToContentfulPaint uses a compositor-side
+// timestamp.
+class PerformancePaintTiming final : public PerformanceEntry {
+ public:
+ PerformancePaintTiming(Performance* aPerformance, const nsAString& aName,
+ const TimeStamp& aStartTime);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PerformancePaintTiming,
+ PerformanceEntry)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ DOMHighResTimeStamp StartTime() const override;
+
+ size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ ~PerformancePaintTiming();
+ RefPtr<Performance> mPerformance;
+
+ const TimeStamp mRawStartTime;
+ mutable Maybe<DOMHighResTimeStamp> mCachedStartTime;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_PerformanacePaintTiming_h___ */
diff --git a/dom/performance/PerformanceResourceTiming.cpp b/dom/performance/PerformanceResourceTiming.cpp
new file mode 100644
index 0000000000..cfb91b2980
--- /dev/null
+++ b/dom/performance/PerformanceResourceTiming.cpp
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceResourceTiming.h"
+#include "mozilla/dom/PerformanceResourceTimingBinding.h"
+#include "nsNetUtil.h"
+#include "nsArrayUtils.h"
+
+using namespace mozilla::dom;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PerformanceResourceTiming, PerformanceEntry,
+ mPerformance)
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(PerformanceResourceTiming,
+ PerformanceEntry)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceResourceTiming)
+NS_INTERFACE_MAP_END_INHERITING(PerformanceEntry)
+
+NS_IMPL_ADDREF_INHERITED(PerformanceResourceTiming, PerformanceEntry)
+NS_IMPL_RELEASE_INHERITED(PerformanceResourceTiming, PerformanceEntry)
+
+PerformanceResourceTiming::PerformanceResourceTiming(
+ UniquePtr<PerformanceTimingData>&& aPerformanceTiming,
+ Performance* aPerformance, const nsAString& aName)
+ : PerformanceEntry(aPerformance->GetParentObject(), aName, u"resource"_ns),
+ mTimingData(std::move(aPerformanceTiming)),
+ mPerformance(aPerformance) {
+ MOZ_RELEASE_ASSERT(mTimingData);
+ MOZ_ASSERT(aPerformance, "Parent performance object should be provided");
+ if (NS_IsMainThread()) {
+ // Used to check if an addon content script has access to this timing.
+ // We don't need it in workers, and ignore mOriginalURI if null.
+ NS_NewURI(getter_AddRefs(mOriginalURI), aName);
+ }
+}
+
+PerformanceResourceTiming::~PerformanceResourceTiming() = default;
+
+DOMHighResTimeStamp PerformanceResourceTiming::FetchStart() const {
+ if (mTimingData->TimingAllowed()) {
+ return mTimingData->FetchStartHighRes(mPerformance);
+ }
+ return StartTime();
+}
+
+DOMHighResTimeStamp PerformanceResourceTiming::StartTime() const {
+ // Force the start time to be the earliest of:
+ // - RedirectStart
+ // - WorkerStart
+ // - AsyncOpen
+ // Ignore zero values. The RedirectStart and WorkerStart values
+ // can come from earlier redirected channels prior to the AsyncOpen
+ // time being recorded.
+ if (mCachedStartTime.isNothing()) {
+ DOMHighResTimeStamp redirect =
+ mTimingData->RedirectStartHighRes(mPerformance);
+ redirect = redirect ? redirect : DBL_MAX;
+
+ DOMHighResTimeStamp worker = mTimingData->WorkerStartHighRes(mPerformance);
+ worker = worker ? worker : DBL_MAX;
+
+ DOMHighResTimeStamp asyncOpen = mTimingData->AsyncOpenHighRes(mPerformance);
+
+ mCachedStartTime.emplace(std::min(asyncOpen, std::min(redirect, worker)));
+ }
+ return mCachedStartTime.value();
+}
+
+JSObject* PerformanceResourceTiming::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceResourceTiming_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+size_t PerformanceResourceTiming::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t PerformanceResourceTiming::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return PerformanceEntry::SizeOfExcludingThis(aMallocSizeOf) +
+ mInitiatorType.SizeOfExcludingThisIfUnshared(aMallocSizeOf) +
+ mTimingData->NextHopProtocol().SizeOfExcludingThisIfUnshared(
+ aMallocSizeOf);
+}
+
+void PerformanceResourceTiming::GetServerTiming(
+ nsTArray<RefPtr<PerformanceServerTiming>>& aRetval,
+ nsIPrincipal& aSubjectPrincipal) {
+ aRetval.Clear();
+ if (!TimingAllowedForCaller(aSubjectPrincipal)) {
+ return;
+ }
+
+ nsTArray<nsCOMPtr<nsIServerTiming>> serverTimingArray =
+ mTimingData->GetServerTiming();
+ uint32_t length = serverTimingArray.Length();
+ for (uint32_t index = 0; index < length; ++index) {
+ nsCOMPtr<nsIServerTiming> serverTiming = serverTimingArray.ElementAt(index);
+ MOZ_ASSERT(serverTiming);
+
+ aRetval.AppendElement(
+ new PerformanceServerTiming(GetParentObject(), serverTiming));
+ }
+}
+
+bool PerformanceResourceTiming::TimingAllowedForCaller(
+ nsIPrincipal& aCaller) const {
+ if (mTimingData->TimingAllowed()) {
+ return true;
+ }
+
+ // Check if the addon has permission to access the cross-origin resource.
+ return mOriginalURI &&
+ BasePrincipal::Cast(&aCaller)->AddonAllowsLoad(mOriginalURI);
+}
+
+bool PerformanceResourceTiming::ReportRedirectForCaller(
+ nsIPrincipal& aCaller, bool aEnsureSameOriginAndIgnoreTAO) const {
+ if (mTimingData->ShouldReportCrossOriginRedirect(
+ aEnsureSameOriginAndIgnoreTAO)) {
+ return true;
+ }
+
+ // Only report cross-origin redirect if the addon has <all_urls> permission.
+ return BasePrincipal::Cast(&aCaller)->AddonHasPermission(
+ nsGkAtoms::all_urlsPermission);
+}
diff --git a/dom/performance/PerformanceResourceTiming.h b/dom/performance/PerformanceResourceTiming.h
new file mode 100644
index 0000000000..baa5953d2d
--- /dev/null
+++ b/dom/performance/PerformanceResourceTiming.h
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceResourceTiming_h___
+#define mozilla_dom_PerformanceResourceTiming_h___
+
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "Performance.h"
+#include "PerformanceEntry.h"
+#include "PerformanceServerTiming.h"
+#include "PerformanceTiming.h"
+
+namespace mozilla::dom {
+#define IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(name) \
+ DOMHighResTimeStamp name(nsIPrincipal& aSubjectPrincipal) const { \
+ bool allowed = !mTimingData->RedirectCountReal() \
+ ? TimingAllowedForCaller(aSubjectPrincipal) \
+ : ReportRedirectForCaller(aSubjectPrincipal, false); \
+ return allowed ? mTimingData->name##HighRes(mPerformance) : 0; \
+ }
+
+#define IMPL_RESOURCE_TIMING_TAO_PROTECTED_SIZE_PROP(name) \
+ uint64_t name(nsIPrincipal& aSubjectPrincipal) const { \
+ bool allowed = !mTimingData->RedirectCountReal() \
+ ? TimingAllowedForCaller(aSubjectPrincipal) \
+ : ReportRedirectForCaller(aSubjectPrincipal, false); \
+ return allowed ? mTimingData->name() : 0; \
+ }
+
+// http://www.w3.org/TR/resource-timing/#performanceresourcetiming
+class PerformanceResourceTiming : public PerformanceEntry {
+ public:
+ using TimeStamp = mozilla::TimeStamp;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(
+ PerformanceResourceTiming, PerformanceEntry)
+
+ // aPerformanceTimingData and aPerformance must be non-null
+ PerformanceResourceTiming(
+ UniquePtr<PerformanceTimingData>&& aPerformanceTimingData,
+ Performance* aPerformance, const nsAString& aName);
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual DOMHighResTimeStamp StartTime() const override;
+
+ virtual DOMHighResTimeStamp Duration() const override {
+ return ResponseEnd() - StartTime();
+ }
+
+ void GetInitiatorType(nsAString& aInitiatorType) const {
+ aInitiatorType = mInitiatorType;
+ }
+
+ void SetInitiatorType(const nsAString& aInitiatorType) {
+ mInitiatorType = aInitiatorType;
+ }
+
+ void GetNextHopProtocol(nsAString& aNextHopProtocol) const {
+ if (mTimingData->TimingAllowed()) {
+ aNextHopProtocol = mTimingData->NextHopProtocol();
+ }
+ }
+
+ DOMHighResTimeStamp WorkerStart() const {
+ return mTimingData->WorkerStartHighRes(mPerformance);
+ }
+
+ DOMHighResTimeStamp FetchStart() const;
+
+ DOMHighResTimeStamp RedirectStart(nsIPrincipal& aSubjectPrincipal,
+ bool aEnsureSameOriginAndIgnoreTAO) const {
+ // We have to check if all the redirect URIs whether had the same origin or
+ // different origins with TAO headers set (since there is no check in
+ // RedirectStartHighRes())
+ return ReportRedirectForCaller(aSubjectPrincipal,
+ aEnsureSameOriginAndIgnoreTAO)
+ ? mTimingData->RedirectStartHighRes(mPerformance)
+ : 0;
+ }
+
+ virtual DOMHighResTimeStamp RedirectStart(
+ nsIPrincipal& aSubjectPrincipal) const {
+ return RedirectStart(aSubjectPrincipal,
+ false /* aEnsureSameOriginAndIgnoreTAO */);
+ }
+
+ DOMHighResTimeStamp RedirectEnd(nsIPrincipal& aSubjectPrincipal,
+ bool aEnsureSameOriginAndIgnoreTAO) const {
+ // We have to check if all the redirect URIs whether had the same origin or
+ // different origins with TAO headers set (since there is no check in
+ // RedirectEndHighRes())
+ return ReportRedirectForCaller(aSubjectPrincipal,
+ aEnsureSameOriginAndIgnoreTAO)
+ ? mTimingData->RedirectEndHighRes(mPerformance)
+ : 0;
+ }
+
+ virtual DOMHighResTimeStamp RedirectEnd(
+ nsIPrincipal& aSubjectPrincipal) const {
+ return RedirectEnd(aSubjectPrincipal,
+ false /* aEnsureSameOriginAndIgnoreTAO */);
+ }
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(DomainLookupStart)
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(DomainLookupEnd)
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(ConnectStart)
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(ConnectEnd)
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(RequestStart)
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(ResponseStart)
+
+ DOMHighResTimeStamp ResponseEnd() const {
+ return mTimingData->ResponseEndHighRes(mPerformance);
+ }
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_TIMING_PROP(SecureConnectionStart)
+
+ virtual const PerformanceResourceTiming* ToResourceTiming() const override {
+ return this;
+ }
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_SIZE_PROP(TransferSize)
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_SIZE_PROP(EncodedBodySize)
+
+ IMPL_RESOURCE_TIMING_TAO_PROTECTED_SIZE_PROP(DecodedBodySize)
+
+ void GetServerTiming(nsTArray<RefPtr<PerformanceServerTiming>>& aRetval,
+ nsIPrincipal& aSubjectPrincipal);
+
+ size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const override;
+
+ protected:
+ virtual ~PerformanceResourceTiming();
+
+ size_t SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const override;
+
+ // Check if caller has access to cross-origin timings, either by the rules
+ // from the spec, or based on addon permissions.
+ bool TimingAllowedForCaller(nsIPrincipal& aCaller) const;
+
+ // Check if cross-origin redirects should be reported to the caller.
+ bool ReportRedirectForCaller(nsIPrincipal& aCaller,
+ bool aEnsureSameOriginAndIgnoreTAO) const;
+
+ nsString mInitiatorType;
+ const UniquePtr<PerformanceTimingData> mTimingData; // always non-null
+ RefPtr<Performance> mPerformance;
+
+ // The same initial requested URI as the `name` attribute.
+ nsCOMPtr<nsIURI> mOriginalURI;
+
+ private:
+ mutable Maybe<DOMHighResTimeStamp> mCachedStartTime;
+};
+
+} // namespace mozilla::dom
+
+#endif /* mozilla_dom_PerformanceResourceTiming_h___ */
diff --git a/dom/performance/PerformanceServerTiming.cpp b/dom/performance/PerformanceServerTiming.cpp
new file mode 100644
index 0000000000..e5f056652d
--- /dev/null
+++ b/dom/performance/PerformanceServerTiming.cpp
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceServerTiming.h"
+#include "nsITimedChannel.h"
+
+#include "mozilla/dom/PerformanceServerTimingBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PerformanceServerTiming, mParent)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PerformanceServerTiming)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PerformanceServerTiming)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PerformanceServerTiming)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* PerformanceServerTiming::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return mozilla::dom::PerformanceServerTiming_Binding::Wrap(aCx, this,
+ aGivenProto);
+}
+
+void PerformanceServerTiming::GetName(nsAString& aName) const {
+ aName.Truncate();
+
+ if (!mServerTiming) {
+ return;
+ }
+
+ nsAutoCString name;
+ if (NS_WARN_IF(NS_FAILED(mServerTiming->GetName(name)))) {
+ return;
+ }
+
+ aName.Assign(NS_ConvertUTF8toUTF16(name));
+}
+
+DOMHighResTimeStamp PerformanceServerTiming::Duration() const {
+ if (!mServerTiming) {
+ return 0;
+ }
+
+ double duration = 0;
+ if (NS_WARN_IF(NS_FAILED(mServerTiming->GetDuration(&duration)))) {
+ return 0;
+ }
+
+ return duration;
+}
+
+void PerformanceServerTiming::GetDescription(nsAString& aDescription) const {
+ if (!mServerTiming) {
+ return;
+ }
+
+ nsAutoCString description;
+ if (NS_WARN_IF(NS_FAILED(mServerTiming->GetDescription(description)))) {
+ return;
+ }
+
+ aDescription.Assign(NS_ConvertUTF8toUTF16(description));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceServerTiming.h b/dom/performance/PerformanceServerTiming.h
new file mode 100644
index 0000000000..27b85d29fd
--- /dev/null
+++ b/dom/performance/PerformanceServerTiming.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceServerTiming_h
+#define mozilla_dom_PerformanceServerTiming_h
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsWrapperCache.h"
+#include "nsString.h"
+
+class nsIServerTiming;
+class nsISupports;
+
+namespace mozilla::dom {
+
+class PerformanceServerTiming final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ PerformanceServerTiming(nsISupports* aParent, nsIServerTiming* aServerTiming)
+ : mParent(aParent), mServerTiming(aServerTiming) {
+ MOZ_ASSERT(mServerTiming);
+ }
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PerformanceServerTiming)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ void GetName(nsAString& aName) const;
+
+ DOMHighResTimeStamp Duration() const;
+
+ void GetDescription(nsAString& aDescription) const;
+
+ private:
+ ~PerformanceServerTiming() = default;
+
+ nsCOMPtr<nsISupports> mParent;
+ nsCOMPtr<nsIServerTiming> mServerTiming;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PerformanceServerTiming_h
diff --git a/dom/performance/PerformanceService.cpp b/dom/performance/PerformanceService.cpp
new file mode 100644
index 0000000000..746d278b77
--- /dev/null
+++ b/dom/performance/PerformanceService.cpp
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceService.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "prtime.h"
+
+namespace mozilla::dom {
+
+static StaticRefPtr<PerformanceService> gPerformanceService;
+static StaticMutex gPerformanceServiceMutex MOZ_UNANNOTATED;
+
+/* static */
+PerformanceService* PerformanceService::GetOrCreate() {
+ StaticMutexAutoLock al(gPerformanceServiceMutex);
+
+ if (!gPerformanceService) {
+ gPerformanceService = new PerformanceService();
+ ClearOnShutdown(&gPerformanceService);
+ }
+
+ return gPerformanceService;
+}
+
+DOMHighResTimeStamp PerformanceService::TimeOrigin(
+ const TimeStamp& aCreationTimeStamp) const {
+ return (aCreationTimeStamp - mCreationTimeStamp).ToMilliseconds() +
+ (mCreationEpochTime / PR_USEC_PER_MSEC);
+}
+
+PerformanceService::PerformanceService() {
+ mCreationTimeStamp = TimeStamp::Now();
+ mCreationEpochTime = PR_Now();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceService.h b/dom/performance/PerformanceService.h
new file mode 100644
index 0000000000..80ded9b180
--- /dev/null
+++ b/dom/performance/PerformanceService.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef dom_performance_PerformanceService_h
+#define dom_performance_PerformanceService_h
+
+#include "mozilla/TimeStamp.h"
+#include "nsCOMPtr.h"
+#include "nsDOMNavigationTiming.h"
+
+namespace mozilla::dom {
+
+// This class is thread-safe.
+
+// We use this singleton for having the correct value of performance.timeOrigin.
+// This value must be calculated on top of the pair:
+// - mCreationTimeStamp (monotonic clock)
+// - mCreationEpochTime (unix epoch time)
+// These 2 values must be taken "at the same time" in order to be used
+// correctly.
+
+class PerformanceService {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PerformanceService)
+
+ static PerformanceService* GetOrCreate();
+
+ DOMHighResTimeStamp TimeOrigin(const TimeStamp& aCreationTimeStamp) const;
+
+ private:
+ PerformanceService();
+ ~PerformanceService() = default;
+
+ TimeStamp mCreationTimeStamp;
+ PRTime mCreationEpochTime;
+};
+
+} // namespace mozilla::dom
+
+#endif // dom_performance_PerformanceService_h
diff --git a/dom/performance/PerformanceStorage.h b/dom/performance/PerformanceStorage.h
new file mode 100644
index 0000000000..b2eee47127
--- /dev/null
+++ b/dom/performance/PerformanceStorage.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceStorage_h
+#define mozilla_dom_PerformanceStorage_h
+
+#include "nsISupportsImpl.h"
+
+class nsIHttpChannel;
+class nsITimedChannel;
+
+namespace mozilla::dom {
+
+class PerformanceTimingData;
+
+class PerformanceStorage {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void AddEntry(nsIHttpChannel* aChannel,
+ nsITimedChannel* aTimedChannel) = 0;
+ virtual void AddEntry(const nsString& entryName,
+ const nsString& initiatorType,
+ UniquePtr<PerformanceTimingData>&& aData) = 0;
+
+ protected:
+ virtual ~PerformanceStorage() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PerformanceStorage_h
diff --git a/dom/performance/PerformanceStorageWorker.cpp b/dom/performance/PerformanceStorageWorker.cpp
new file mode 100644
index 0000000000..4fb4815121
--- /dev/null
+++ b/dom/performance/PerformanceStorageWorker.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceStorageWorker.h"
+#include "Performance.h"
+#include "PerformanceResourceTiming.h"
+#include "PerformanceTiming.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerScope.h"
+
+namespace mozilla::dom {
+
+class PerformanceProxyData {
+ public:
+ PerformanceProxyData(UniquePtr<PerformanceTimingData>&& aData,
+ const nsAString& aInitiatorType,
+ const nsAString& aEntryName)
+ : mData(std::move(aData)),
+ mInitiatorType(aInitiatorType),
+ mEntryName(aEntryName) {
+ MOZ_RELEASE_ASSERT(mData);
+ }
+
+ UniquePtr<PerformanceTimingData> mData; // always non-null
+ nsString mInitiatorType;
+ nsString mEntryName;
+};
+
+namespace {
+
+// Here we use control runnable because this code must be executed also when in
+// a sync event loop
+class PerformanceEntryAdder final : public WorkerControlRunnable {
+ public:
+ PerformanceEntryAdder(WorkerPrivate* aWorkerPrivate,
+ PerformanceStorageWorker* aStorage,
+ UniquePtr<PerformanceProxyData>&& aData)
+ : WorkerControlRunnable(aWorkerPrivate, "PerformanceEntryAdder",
+ WorkerThread),
+ mStorage(aStorage),
+ mData(std::move(aData)) {}
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override {
+ mStorage->AddEntryOnWorker(std::move(mData));
+ return true;
+ }
+
+ nsresult Cancel() override {
+ mStorage->ShutdownOnWorker();
+ return NS_OK;
+ }
+
+ bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; }
+
+ void PostDispatch(WorkerPrivate* aWorkerPrivate,
+ bool aDispatchResult) override {}
+
+ private:
+ RefPtr<PerformanceStorageWorker> mStorage;
+ UniquePtr<PerformanceProxyData> mData;
+};
+
+} // namespace
+
+/* static */
+already_AddRefed<PerformanceStorageWorker> PerformanceStorageWorker::Create(
+ WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+
+ RefPtr<PerformanceStorageWorker> storage = new PerformanceStorageWorker();
+
+ MutexAutoLock lock(storage->mMutex); // for thread-safety analysis
+ storage->mWorkerRef = WeakWorkerRef::Create(
+ aWorkerPrivate, [storage]() { storage->ShutdownOnWorker(); });
+
+ // PerformanceStorageWorker is created at the creation time of the worker.
+ MOZ_ASSERT(storage->mWorkerRef);
+
+ return storage.forget();
+}
+
+PerformanceStorageWorker::PerformanceStorageWorker()
+ : mMutex("PerformanceStorageWorker::mMutex") {}
+
+PerformanceStorageWorker::~PerformanceStorageWorker() = default;
+
+void PerformanceStorageWorker::AddEntry(nsIHttpChannel* aChannel,
+ nsITimedChannel* aTimedChannel) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MutexAutoLock lock(mMutex);
+
+ if (!mWorkerRef) {
+ return;
+ }
+
+ // If we have mWorkerRef, we haven't received the WorkerRef notification and
+ // we haven't yet call ShutdownOnWorker, which uses the mutex.
+ WorkerPrivate* workerPrivate = mWorkerRef->GetUnsafePrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ nsAutoString initiatorType;
+ nsAutoString entryName;
+
+ UniquePtr<PerformanceTimingData> performanceTimingData(
+ PerformanceTimingData::Create(aTimedChannel, aChannel, 0, initiatorType,
+ entryName));
+ if (!performanceTimingData) {
+ return;
+ }
+
+ UniquePtr<PerformanceProxyData> data(new PerformanceProxyData(
+ std::move(performanceTimingData), initiatorType, entryName));
+
+ RefPtr<PerformanceEntryAdder> r =
+ new PerformanceEntryAdder(workerPrivate, this, std::move(data));
+ Unused << NS_WARN_IF(!r->Dispatch());
+}
+
+void PerformanceStorageWorker::AddEntry(
+ const nsString& aEntryName, const nsString& aInitiatorType,
+ UniquePtr<PerformanceTimingData>&& aData) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ if (!aData) {
+ return;
+ }
+
+ UniquePtr<PerformanceProxyData> data = MakeUnique<PerformanceProxyData>(
+ std::move(aData), aInitiatorType, aEntryName);
+
+ AddEntryOnWorker(std::move(data));
+}
+
+void PerformanceStorageWorker::ShutdownOnWorker() {
+ MutexAutoLock lock(mMutex);
+
+ if (!mWorkerRef) {
+ return;
+ }
+
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ mWorkerRef = nullptr;
+}
+
+void PerformanceStorageWorker::AddEntryOnWorker(
+ UniquePtr<PerformanceProxyData>&& aData) {
+ RefPtr<Performance> performance;
+ UniquePtr<PerformanceProxyData> data = std::move(aData);
+
+ {
+ MutexAutoLock lock(mMutex);
+
+ if (!mWorkerRef) {
+ return;
+ }
+
+ // We must have the workerPrivate because it is available until a
+ // notification is received by WorkerRef and we use mutex to make the code
+ // protected.
+ WorkerPrivate* workerPrivate = mWorkerRef->GetPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ WorkerGlobalScope* scope = workerPrivate->GlobalScope();
+ performance = scope->GetPerformance();
+ }
+
+ if (NS_WARN_IF(!performance)) {
+ return;
+ }
+
+ RefPtr<PerformanceResourceTiming> performanceEntry =
+ new PerformanceResourceTiming(std::move(data->mData), performance,
+ data->mEntryName);
+ performanceEntry->SetInitiatorType(data->mInitiatorType);
+
+ performance->InsertResourceEntry(performanceEntry);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceStorageWorker.h b/dom/performance/PerformanceStorageWorker.h
new file mode 100644
index 0000000000..e585069112
--- /dev/null
+++ b/dom/performance/PerformanceStorageWorker.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceStorageWorker_h
+#define mozilla_dom_PerformanceStorageWorker_h
+
+#include "mozilla/Mutex.h"
+#include "PerformanceStorage.h"
+
+namespace mozilla::dom {
+
+class WeakWorkerRef;
+class WorkerPrivate;
+
+class PerformanceProxyData;
+
+class PerformanceStorageWorker final : public PerformanceStorage {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PerformanceStorageWorker, override)
+
+ static already_AddRefed<PerformanceStorageWorker> Create(
+ WorkerPrivate* aWorkerPrivate);
+
+ void ShutdownOnWorker();
+
+ void AddEntry(nsIHttpChannel* aChannel,
+ nsITimedChannel* aTimedChannel) override;
+ void AddEntry(const nsString& aEntryName, const nsString& aInitiatorType,
+ UniquePtr<PerformanceTimingData>&& aData) override;
+ void AddEntryOnWorker(UniquePtr<PerformanceProxyData>&& aData);
+
+ private:
+ PerformanceStorageWorker();
+ ~PerformanceStorageWorker();
+
+ Mutex mMutex;
+
+ // Protected by mutex.
+ // Created and released on worker-thread. Used also on main-thread.
+ RefPtr<WeakWorkerRef> mWorkerRef MOZ_GUARDED_BY(mMutex);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PerformanceStorageWorker_h
diff --git a/dom/performance/PerformanceTiming.cpp b/dom/performance/PerformanceTiming.cpp
new file mode 100644
index 0000000000..546783d686
--- /dev/null
+++ b/dom/performance/PerformanceTiming.cpp
@@ -0,0 +1,677 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceTiming.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/PerformanceTimingBinding.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/Telemetry.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIHttpChannel.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "nsITimedChannel.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PerformanceTiming, mPerformance)
+
+/* static */
+PerformanceTimingData* PerformanceTimingData::Create(
+ nsITimedChannel* aTimedChannel, nsIHttpChannel* aChannel,
+ DOMHighResTimeStamp aZeroTime, nsAString& aInitiatorType,
+ nsAString& aEntryName) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Check if resource timing is prefed off.
+ if (!StaticPrefs::dom_enable_resource_timing()) {
+ return nullptr;
+ }
+
+ if (!aChannel || !aTimedChannel) {
+ return nullptr;
+ }
+
+ bool reportTiming = true;
+ aTimedChannel->GetReportResourceTiming(&reportTiming);
+
+ if (!reportTiming) {
+ return nullptr;
+ }
+
+ aTimedChannel->GetInitiatorType(aInitiatorType);
+
+ // If the initiator type had no valid value, then set it to the default
+ // ("other") value.
+ if (aInitiatorType.IsEmpty()) {
+ aInitiatorType = u"other"_ns;
+ }
+
+ // According to the spec, "The name attribute must return the resolved URL
+ // of the requested resource. This attribute must not change even if the
+ // fetch redirected to a different URL."
+ nsCOMPtr<nsIURI> originalURI;
+ aChannel->GetOriginalURI(getter_AddRefs(originalURI));
+
+ nsAutoCString name;
+ originalURI->GetSpec(name);
+ CopyUTF8toUTF16(name, aEntryName);
+
+ // The nsITimedChannel argument will be used to gather all the timings.
+ // The nsIHttpChannel argument will be used to check if any cross-origin
+ // redirects occurred.
+ // The last argument is the "zero time" (offset). Since we don't want
+ // any offset for the resource timing, this will be set to "0" - the
+ // resource timing returns a relative timing (no offset).
+ return new PerformanceTimingData(aTimedChannel, aChannel, 0);
+}
+
+PerformanceTiming::PerformanceTiming(Performance* aPerformance,
+ nsITimedChannel* aChannel,
+ nsIHttpChannel* aHttpChannel,
+ DOMHighResTimeStamp aZeroTime)
+ : mPerformance(aPerformance) {
+ MOZ_ASSERT(aPerformance, "Parent performance object should be provided");
+
+ mTimingData.reset(new PerformanceTimingData(
+ aChannel, aHttpChannel,
+ nsRFPService::ReduceTimePrecisionAsMSecs(
+ aZeroTime, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType())));
+
+ // Non-null aHttpChannel implies that this PerformanceTiming object is being
+ // used for subresources, which is irrelevant to this probe.
+ if (!aHttpChannel && StaticPrefs::dom_enable_performance() &&
+ IsTopLevelContentDocument()) {
+ glean::performance_time::response_start.AccumulateRawDuration(
+ TimeDuration::FromMilliseconds(
+ mTimingData->ResponseStartHighRes(aPerformance) -
+ mTimingData->ZeroTime()));
+ }
+}
+
+// Copy the timing info from the channel so we don't need to keep the channel
+// alive just to get the timestamps.
+PerformanceTimingData::PerformanceTimingData(nsITimedChannel* aChannel,
+ nsIHttpChannel* aHttpChannel,
+ DOMHighResTimeStamp aZeroTime)
+ : mZeroTime(0.0),
+ mFetchStart(0.0),
+ mEncodedBodySize(0),
+ mTransferSize(0),
+ mDecodedBodySize(0),
+ mRedirectCount(0),
+ mAllRedirectsSameOrigin(true),
+ mAllRedirectsPassTAO(true),
+ mSecureConnection(false),
+ mTimingAllowed(true),
+ mInitialized(false) {
+ mInitialized = !!aChannel;
+ mZeroTime = aZeroTime;
+
+ if (!StaticPrefs::dom_enable_performance()) {
+ mZeroTime = 0;
+ }
+
+ nsCOMPtr<nsIURI> uri;
+ if (aHttpChannel) {
+ aHttpChannel->GetURI(getter_AddRefs(uri));
+ } else {
+ nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
+ if (httpChannel) {
+ httpChannel->GetURI(getter_AddRefs(uri));
+ }
+ }
+
+ if (uri) {
+ mSecureConnection = uri->SchemeIs("https");
+ }
+
+ if (aChannel) {
+ aChannel->GetAsyncOpen(&mAsyncOpen);
+ aChannel->GetAllRedirectsSameOrigin(&mAllRedirectsSameOrigin);
+ aChannel->GetAllRedirectsPassTimingAllowCheck(&mAllRedirectsPassTAO);
+ aChannel->GetRedirectCount(&mRedirectCount);
+ aChannel->GetRedirectStart(&mRedirectStart);
+ aChannel->GetRedirectEnd(&mRedirectEnd);
+ aChannel->GetDomainLookupStart(&mDomainLookupStart);
+ aChannel->GetDomainLookupEnd(&mDomainLookupEnd);
+ aChannel->GetConnectStart(&mConnectStart);
+ aChannel->GetSecureConnectionStart(&mSecureConnectionStart);
+ aChannel->GetConnectEnd(&mConnectEnd);
+ aChannel->GetRequestStart(&mRequestStart);
+ aChannel->GetResponseStart(&mResponseStart);
+ aChannel->GetCacheReadStart(&mCacheReadStart);
+ aChannel->GetResponseEnd(&mResponseEnd);
+ aChannel->GetCacheReadEnd(&mCacheReadEnd);
+
+ aChannel->GetDispatchFetchEventStart(&mWorkerStart);
+ aChannel->GetHandleFetchEventStart(&mWorkerRequestStart);
+ // TODO: Track when FetchEvent.respondWith() promise resolves as
+ // ServiceWorker interception responseStart?
+ aChannel->GetHandleFetchEventEnd(&mWorkerResponseEnd);
+
+ // The performance timing api essentially requires that the event timestamps
+ // have a strict relation with each other. The truth, however, is the
+ // browser engages in a number of speculative activities that sometimes mean
+ // connections and lookups begin at different times. Workaround that here by
+ // clamping these values to what we expect FetchStart to be. This means the
+ // later of AsyncOpen or WorkerStart times.
+ if (!mAsyncOpen.IsNull()) {
+ // We want to clamp to the expected FetchStart value. This is later of
+ // the AsyncOpen and WorkerStart values.
+ const TimeStamp* clampTime = &mAsyncOpen;
+ if (!mWorkerStart.IsNull() && mWorkerStart > mAsyncOpen) {
+ clampTime = &mWorkerStart;
+ }
+
+ if (!mDomainLookupStart.IsNull() && mDomainLookupStart < *clampTime) {
+ mDomainLookupStart = *clampTime;
+ }
+
+ if (!mDomainLookupEnd.IsNull() && mDomainLookupEnd < *clampTime) {
+ mDomainLookupEnd = *clampTime;
+ }
+
+ if (!mConnectStart.IsNull() && mConnectStart < *clampTime) {
+ mConnectStart = *clampTime;
+ }
+
+ if (mSecureConnection && !mSecureConnectionStart.IsNull() &&
+ mSecureConnectionStart < *clampTime) {
+ mSecureConnectionStart = *clampTime;
+ }
+
+ if (!mConnectEnd.IsNull() && mConnectEnd < *clampTime) {
+ mConnectEnd = *clampTime;
+ }
+ }
+ }
+
+ // The aHttpChannel argument is null if this PerformanceTiming object is
+ // being used for navigation timing (which is only relevant for documents).
+ // It has a non-null value if this PerformanceTiming object is being used
+ // for resource timing, which can include document loads, both toplevel and
+ // in subframes, and resources linked from a document.
+ if (aHttpChannel) {
+ SetPropertiesFromHttpChannel(aHttpChannel, aChannel);
+ }
+}
+
+PerformanceTimingData::PerformanceTimingData(
+ const IPCPerformanceTimingData& aIPCData)
+ : mNextHopProtocol(aIPCData.nextHopProtocol()),
+ mAsyncOpen(aIPCData.asyncOpen()),
+ mRedirectStart(aIPCData.redirectStart()),
+ mRedirectEnd(aIPCData.redirectEnd()),
+ mDomainLookupStart(aIPCData.domainLookupStart()),
+ mDomainLookupEnd(aIPCData.domainLookupEnd()),
+ mConnectStart(aIPCData.connectStart()),
+ mSecureConnectionStart(aIPCData.secureConnectionStart()),
+ mConnectEnd(aIPCData.connectEnd()),
+ mRequestStart(aIPCData.requestStart()),
+ mResponseStart(aIPCData.responseStart()),
+ mCacheReadStart(aIPCData.cacheReadStart()),
+ mResponseEnd(aIPCData.responseEnd()),
+ mCacheReadEnd(aIPCData.cacheReadEnd()),
+ mWorkerStart(aIPCData.workerStart()),
+ mWorkerRequestStart(aIPCData.workerRequestStart()),
+ mWorkerResponseEnd(aIPCData.workerResponseEnd()),
+ mZeroTime(aIPCData.zeroTime()),
+ mFetchStart(aIPCData.fetchStart()),
+ mEncodedBodySize(aIPCData.encodedBodySize()),
+ mTransferSize(aIPCData.transferSize()),
+ mDecodedBodySize(aIPCData.decodedBodySize()),
+ mRedirectCount(aIPCData.redirectCount()),
+ mAllRedirectsSameOrigin(aIPCData.allRedirectsSameOrigin()),
+ mAllRedirectsPassTAO(aIPCData.allRedirectsPassTAO()),
+ mSecureConnection(aIPCData.secureConnection()),
+ mTimingAllowed(aIPCData.timingAllowed()),
+ mInitialized(aIPCData.initialized()) {
+ for (const auto& serverTimingData : aIPCData.serverTiming()) {
+ RefPtr<nsServerTiming> timing = new nsServerTiming();
+ timing->SetName(serverTimingData.name());
+ timing->SetDuration(serverTimingData.duration());
+ timing->SetDescription(serverTimingData.description());
+ mServerTiming.AppendElement(timing);
+ }
+}
+
+IPCPerformanceTimingData PerformanceTimingData::ToIPC() {
+ nsTArray<IPCServerTiming> ipcServerTiming;
+ for (auto& serverTimingData : mServerTiming) {
+ nsAutoCString name;
+ Unused << serverTimingData->GetName(name);
+ double duration = 0;
+ Unused << serverTimingData->GetDuration(&duration);
+ nsAutoCString description;
+ Unused << serverTimingData->GetDescription(description);
+ ipcServerTiming.AppendElement(IPCServerTiming(name, duration, description));
+ }
+ return IPCPerformanceTimingData(
+ ipcServerTiming, mNextHopProtocol, mAsyncOpen, mRedirectStart,
+ mRedirectEnd, mDomainLookupStart, mDomainLookupEnd, mConnectStart,
+ mSecureConnectionStart, mConnectEnd, mRequestStart, mResponseStart,
+ mCacheReadStart, mResponseEnd, mCacheReadEnd, mWorkerStart,
+ mWorkerRequestStart, mWorkerResponseEnd, mZeroTime, mFetchStart,
+ mEncodedBodySize, mTransferSize, mDecodedBodySize, mRedirectCount,
+ mAllRedirectsSameOrigin, mAllRedirectsPassTAO, mSecureConnection,
+ mTimingAllowed, mInitialized);
+}
+
+void PerformanceTimingData::SetPropertiesFromHttpChannel(
+ nsIHttpChannel* aHttpChannel, nsITimedChannel* aChannel) {
+ MOZ_ASSERT(aHttpChannel);
+
+ nsAutoCString protocol;
+ Unused << aHttpChannel->GetProtocolVersion(protocol);
+ CopyUTF8toUTF16(protocol, mNextHopProtocol);
+
+ Unused << aHttpChannel->GetEncodedBodySize(&mEncodedBodySize);
+ Unused << aHttpChannel->GetTransferSize(&mTransferSize);
+ Unused << aHttpChannel->GetDecodedBodySize(&mDecodedBodySize);
+ if (mDecodedBodySize == 0) {
+ mDecodedBodySize = mEncodedBodySize;
+ }
+
+ mTimingAllowed = CheckAllowedOrigin(aHttpChannel, aChannel);
+ aChannel->GetAllRedirectsPassTimingAllowCheck(&mAllRedirectsPassTAO);
+
+ aChannel->GetNativeServerTiming(mServerTiming);
+}
+
+PerformanceTiming::~PerformanceTiming() = default;
+
+DOMHighResTimeStamp PerformanceTimingData::FetchStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!mFetchStart) {
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ MOZ_ASSERT(!mAsyncOpen.IsNull(),
+ "The fetch start time stamp should always be "
+ "valid if the performance timing is enabled");
+ if (!mAsyncOpen.IsNull()) {
+ if (!mWorkerRequestStart.IsNull() && mWorkerRequestStart > mAsyncOpen) {
+ mFetchStart = TimeStampToDOMHighRes(aPerformance, mWorkerRequestStart);
+ } else {
+ mFetchStart = TimeStampToDOMHighRes(aPerformance, mAsyncOpen);
+ }
+ }
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ mFetchStart, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+DOMTimeMilliSec PerformanceTiming::FetchStart() {
+ return static_cast<int64_t>(mTimingData->FetchStartHighRes(mPerformance));
+}
+
+bool PerformanceTimingData::CheckAllowedOrigin(nsIHttpChannel* aResourceChannel,
+ nsITimedChannel* aChannel) {
+ if (!IsInitialized()) {
+ return false;
+ }
+
+ // Check that the current document passes the ckeck.
+ nsCOMPtr<nsILoadInfo> loadInfo = aResourceChannel->LoadInfo();
+
+ // TYPE_DOCUMENT loads have no loadingPrincipal.
+ if (loadInfo->GetExternalContentPolicyType() ==
+ ExtContentPolicy::TYPE_DOCUMENT) {
+ return true;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = loadInfo->GetLoadingPrincipal();
+
+ // Check if the resource is either same origin as the page that started
+ // the load, or if the response contains the proper Timing-Allow-Origin
+ // header with the domain of the page that started the load.
+ return aChannel->TimingAllowCheck(principal);
+}
+
+uint8_t PerformanceTimingData::GetRedirectCount() const {
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return 0;
+ }
+ if (!mAllRedirectsSameOrigin) {
+ return 0;
+ }
+ return mRedirectCount;
+}
+
+bool PerformanceTimingData::ShouldReportCrossOriginRedirect(
+ bool aEnsureSameOriginAndIgnoreTAO) const {
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return false;
+ }
+
+ if (!mTimingAllowed || mRedirectCount == 0) {
+ return false;
+ }
+
+ // If the redirect count is 0, or if one of the cross-origin
+ // redirects doesn't have the proper Timing-Allow-Origin header,
+ // then RedirectStart and RedirectEnd will be set to zero
+ return aEnsureSameOriginAndIgnoreTAO ? mAllRedirectsSameOrigin
+ : mAllRedirectsPassTAO;
+}
+
+DOMHighResTimeStamp PerformanceTimingData::AsyncOpenHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized() ||
+ mAsyncOpen.IsNull()) {
+ return mZeroTime;
+ }
+ DOMHighResTimeStamp rawValue =
+ TimeStampToDOMHighRes(aPerformance, mAsyncOpen);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+DOMHighResTimeStamp PerformanceTimingData::WorkerStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized() ||
+ mWorkerStart.IsNull()) {
+ return mZeroTime;
+ }
+ DOMHighResTimeStamp rawValue =
+ TimeStampToDOMHighRes(aPerformance, mWorkerStart);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+/**
+ * RedirectStartHighRes() is used by both the navigation timing and the
+ * resource timing. Since, navigation timing and resource timing check and
+ * interpret cross-domain redirects in a different manner,
+ * RedirectStartHighRes() will make no checks for cross-domain redirect.
+ * It's up to the consumers of this method (PerformanceTiming::RedirectStart()
+ * and PerformanceResourceTiming::RedirectStart() to make such verifications.
+ *
+ * @return a valid timing if the Performance Timing is enabled
+ */
+DOMHighResTimeStamp PerformanceTimingData::RedirectStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, mRedirectStart);
+}
+
+DOMTimeMilliSec PerformanceTiming::RedirectStart() {
+ if (!mTimingData->IsInitialized()) {
+ return 0;
+ }
+ // We have to check if all the redirect URIs had the same origin (since there
+ // is no check in RedirectStartHighRes())
+ if (mTimingData->AllRedirectsSameOrigin() &&
+ mTimingData->RedirectCountReal()) {
+ return static_cast<int64_t>(
+ mTimingData->RedirectStartHighRes(mPerformance));
+ }
+ return 0;
+}
+
+/**
+ * RedirectEndHighRes() is used by both the navigation timing and the resource
+ * timing. Since, navigation timing and resource timing check and interpret
+ * cross-domain redirects in a different manner, RedirectEndHighRes() will make
+ * no checks for cross-domain redirect. It's up to the consumers of this method
+ * (PerformanceTiming::RedirectEnd() and
+ * PerformanceResourceTiming::RedirectEnd() to make such verifications.
+ *
+ * @return a valid timing if the Performance Timing is enabled
+ */
+DOMHighResTimeStamp PerformanceTimingData::RedirectEndHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, mRedirectEnd);
+}
+
+DOMTimeMilliSec PerformanceTiming::RedirectEnd() {
+ if (!mTimingData->IsInitialized()) {
+ return 0;
+ }
+ // We have to check if all the redirect URIs had the same origin (since there
+ // is no check in RedirectEndHighRes())
+ if (mTimingData->AllRedirectsSameOrigin() &&
+ mTimingData->RedirectCountReal()) {
+ return static_cast<int64_t>(mTimingData->RedirectEndHighRes(mPerformance));
+ }
+ return 0;
+}
+
+DOMHighResTimeStamp PerformanceTimingData::DomainLookupStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ // Bug 1637985 - DomainLookup information may be useful for fingerprinting.
+ if (aPerformance->ShouldResistFingerprinting()) {
+ return FetchStartHighRes(aPerformance);
+ }
+ return TimeStampToReducedDOMHighResOrFetchStart(aPerformance,
+ mDomainLookupStart);
+}
+
+DOMTimeMilliSec PerformanceTiming::DomainLookupStart() {
+ return static_cast<int64_t>(
+ mTimingData->DomainLookupStartHighRes(mPerformance));
+}
+
+DOMHighResTimeStamp PerformanceTimingData::DomainLookupEndHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ // Bug 1637985 - DomainLookup information may be useful for fingerprinting.
+ if (aPerformance->ShouldResistFingerprinting()) {
+ return FetchStartHighRes(aPerformance);
+ }
+ // Bug 1155008 - nsHttpTransaction is racy. Return DomainLookupStart when null
+ if (mDomainLookupEnd.IsNull()) {
+ return DomainLookupStartHighRes(aPerformance);
+ }
+ DOMHighResTimeStamp rawValue =
+ TimeStampToDOMHighRes(aPerformance, mDomainLookupEnd);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+DOMTimeMilliSec PerformanceTiming::DomainLookupEnd() {
+ return static_cast<int64_t>(
+ mTimingData->DomainLookupEndHighRes(mPerformance));
+}
+
+DOMHighResTimeStamp PerformanceTimingData::ConnectStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ if (mConnectStart.IsNull()) {
+ return DomainLookupEndHighRes(aPerformance);
+ }
+ DOMHighResTimeStamp rawValue =
+ TimeStampToDOMHighRes(aPerformance, mConnectStart);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+DOMTimeMilliSec PerformanceTiming::ConnectStart() {
+ return static_cast<int64_t>(mTimingData->ConnectStartHighRes(mPerformance));
+}
+
+DOMHighResTimeStamp PerformanceTimingData::SecureConnectionStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ if (!mSecureConnection) {
+ return 0; // We use 0 here, because mZeroTime is sometimes set to the
+ // navigation start time.
+ }
+ if (mSecureConnectionStart.IsNull()) {
+ return ConnectStartHighRes(aPerformance);
+ }
+ DOMHighResTimeStamp rawValue =
+ TimeStampToDOMHighRes(aPerformance, mSecureConnectionStart);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+DOMTimeMilliSec PerformanceTiming::SecureConnectionStart() {
+ return static_cast<int64_t>(
+ mTimingData->SecureConnectionStartHighRes(mPerformance));
+}
+
+DOMHighResTimeStamp PerformanceTimingData::ConnectEndHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ // Bug 1155008 - nsHttpTransaction is racy. Return ConnectStart when null
+ if (mConnectEnd.IsNull()) {
+ return ConnectStartHighRes(aPerformance);
+ }
+ DOMHighResTimeStamp rawValue =
+ TimeStampToDOMHighRes(aPerformance, mConnectEnd);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+DOMTimeMilliSec PerformanceTiming::ConnectEnd() {
+ return static_cast<int64_t>(mTimingData->ConnectEndHighRes(mPerformance));
+}
+
+DOMHighResTimeStamp PerformanceTimingData::RequestStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+
+ if (mRequestStart.IsNull()) {
+ mRequestStart = mWorkerRequestStart;
+ }
+
+ return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, mRequestStart);
+}
+
+DOMTimeMilliSec PerformanceTiming::RequestStart() {
+ return static_cast<int64_t>(mTimingData->RequestStartHighRes(mPerformance));
+}
+
+DOMHighResTimeStamp PerformanceTimingData::ResponseStartHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ if (mResponseStart.IsNull() ||
+ (!mCacheReadStart.IsNull() && mCacheReadStart < mResponseStart)) {
+ mResponseStart = mCacheReadStart;
+ }
+
+ if (mResponseStart.IsNull() ||
+ (!mRequestStart.IsNull() && mResponseStart < mRequestStart)) {
+ mResponseStart = mRequestStart;
+ }
+ return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, mResponseStart);
+}
+
+DOMTimeMilliSec PerformanceTiming::ResponseStart() {
+ return static_cast<int64_t>(mTimingData->ResponseStartHighRes(mPerformance));
+}
+
+DOMHighResTimeStamp PerformanceTimingData::ResponseEndHighRes(
+ Performance* aPerformance) {
+ MOZ_ASSERT(aPerformance);
+
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized()) {
+ return mZeroTime;
+ }
+ if (mResponseEnd.IsNull() ||
+ (!mCacheReadEnd.IsNull() && mCacheReadEnd < mResponseEnd)) {
+ mResponseEnd = mCacheReadEnd;
+ }
+ if (mResponseEnd.IsNull()) {
+ mResponseEnd = mWorkerResponseEnd;
+ }
+ // Bug 1155008 - nsHttpTransaction is racy. Return ResponseStart when null
+ if (mResponseEnd.IsNull()) {
+ return ResponseStartHighRes(aPerformance);
+ }
+ DOMHighResTimeStamp rawValue =
+ TimeStampToDOMHighRes(aPerformance, mResponseEnd);
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawValue, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+}
+
+DOMTimeMilliSec PerformanceTiming::ResponseEnd() {
+ return static_cast<int64_t>(mTimingData->ResponseEndHighRes(mPerformance));
+}
+
+JSObject* PerformanceTiming::WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PerformanceTiming_Binding::Wrap(cx, this, aGivenProto);
+}
+
+bool PerformanceTiming::IsTopLevelContentDocument() const {
+ nsCOMPtr<Document> document = mPerformance->GetDocumentIfCurrent();
+ if (!document) {
+ return false;
+ }
+
+ if (BrowsingContext* bc = document->GetBrowsingContext()) {
+ return bc->IsTopContent();
+ }
+ return false;
+}
+
+nsTArray<nsCOMPtr<nsIServerTiming>> PerformanceTimingData::GetServerTiming() {
+ if (!StaticPrefs::dom_enable_performance() || !IsInitialized() ||
+ !TimingAllowed()) {
+ return nsTArray<nsCOMPtr<nsIServerTiming>>();
+ }
+
+ return mServerTiming.Clone();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceTiming.h b/dom/performance/PerformanceTiming.h
new file mode 100644
index 0000000000..5c14c009d7
--- /dev/null
+++ b/dom/performance/PerformanceTiming.h
@@ -0,0 +1,595 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceTiming_h
+#define mozilla_dom_PerformanceTiming_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsContentUtils.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsRFPService.h"
+#include "nsWrapperCache.h"
+#include "Performance.h"
+#include "nsITimedChannel.h"
+#include "mozilla/dom/PerformanceTimingTypes.h"
+#include "mozilla/ipc/IPDLParamTraits.h"
+#include "ipc/IPCMessageUtils.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/net/nsServerTiming.h"
+
+class nsIHttpChannel;
+
+namespace mozilla::dom {
+
+class PerformanceTiming;
+
+class PerformanceTimingData final {
+ friend class PerformanceTiming;
+ friend struct mozilla::ipc::IPDLParamTraits<
+ mozilla::dom::PerformanceTimingData>;
+
+ public:
+ PerformanceTimingData() = default; // For deserialization
+ // This can return null.
+ static PerformanceTimingData* Create(nsITimedChannel* aChannel,
+ nsIHttpChannel* aHttpChannel,
+ DOMHighResTimeStamp aZeroTime,
+ nsAString& aInitiatorType,
+ nsAString& aEntryName);
+
+ PerformanceTimingData(nsITimedChannel* aChannel, nsIHttpChannel* aHttpChannel,
+ DOMHighResTimeStamp aZeroTime);
+
+ explicit PerformanceTimingData(const IPCPerformanceTimingData& aIPCData);
+
+ IPCPerformanceTimingData ToIPC();
+
+ void SetPropertiesFromHttpChannel(nsIHttpChannel* aHttpChannel,
+ nsITimedChannel* aChannel);
+
+ bool IsInitialized() const { return mInitialized; }
+
+ const nsString& NextHopProtocol() const { return mNextHopProtocol; }
+
+ uint64_t TransferSize() const { return mTransferSize; }
+
+ uint64_t EncodedBodySize() const { return mEncodedBodySize; }
+
+ uint64_t DecodedBodySize() const { return mDecodedBodySize; }
+
+ /**
+ * @param aStamp
+ * The TimeStamp recorded for a specific event. This TimeStamp can
+ * be null.
+ * @return the duration of an event with a given TimeStamp, relative to the
+ * navigationStart TimeStamp (the moment the user landed on the
+ * page), if the given TimeStamp is valid. Otherwise, it will return
+ * the FetchStart timing value.
+ */
+ inline DOMHighResTimeStamp TimeStampToReducedDOMHighResOrFetchStart(
+ Performance* aPerformance, TimeStamp aStamp) {
+ MOZ_ASSERT(aPerformance);
+
+ if (aStamp.IsNull()) {
+ return FetchStartHighRes(aPerformance);
+ }
+
+ DOMHighResTimeStamp rawTimestamp =
+ TimeStampToDOMHighRes(aPerformance, aStamp);
+
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ rawTimestamp, aPerformance->GetRandomTimelineSeed(),
+ aPerformance->GetRTPCallerType());
+ }
+
+ /**
+ * The nsITimedChannel records an absolute timestamp for each event.
+ * The nsDOMNavigationTiming will record the moment when the user landed on
+ * the page. This is a window.performance unique timestamp, so it can be used
+ * for all the events (navigation timing and resource timing events).
+ *
+ * The algorithm operates in 2 steps:
+ * 1. The first step is to subtract the two timestamps: the argument (the
+ * event's timestamp) and the navigation start timestamp. This will result in
+ * a relative timestamp of the event (relative to the navigation start -
+ * window.performance.timing.navigationStart).
+ * 2. The second step is to add any required offset (the mZeroTime). For now,
+ * this offset value is either 0 (for the resource timing), or equal to
+ * "performance.navigationStart" (for navigation timing).
+ * For the resource timing, mZeroTime is set to 0, causing the result to be a
+ * relative time.
+ * For the navigation timing, mZeroTime is set to
+ * "performance.navigationStart" causing the result be an absolute time.
+ *
+ * @param aStamp
+ * The TimeStamp recorded for a specific event. This TimeStamp can't
+ * be null.
+ * @return number of milliseconds value as one of:
+ * - relative to the navigation start time, time the user has landed on the
+ * page
+ * - an absolute wall clock time since the unix epoch
+ */
+ inline DOMHighResTimeStamp TimeStampToDOMHighRes(Performance* aPerformance,
+ TimeStamp aStamp) const {
+ MOZ_ASSERT(aPerformance);
+ MOZ_ASSERT(!aStamp.IsNull());
+
+ TimeDuration duration = aStamp - aPerformance->CreationTimeStamp();
+ return duration.ToMilliseconds() + mZeroTime;
+ }
+
+ // The last channel's AsyncOpen time. This may occur before the FetchStart
+ // in some cases.
+ DOMHighResTimeStamp AsyncOpenHighRes(Performance* aPerformance);
+
+ // High resolution (used by resource timing)
+ DOMHighResTimeStamp WorkerStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp FetchStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp RedirectStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp RedirectEndHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp DomainLookupStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp DomainLookupEndHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp ConnectStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp SecureConnectionStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp ConnectEndHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp RequestStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp ResponseStartHighRes(Performance* aPerformance);
+ DOMHighResTimeStamp ResponseEndHighRes(Performance* aPerformance);
+
+ DOMHighResTimeStamp ZeroTime() const { return mZeroTime; }
+
+ uint8_t RedirectCountReal() const { return mRedirectCount; }
+ uint8_t GetRedirectCount() const;
+
+ bool AllRedirectsSameOrigin() const { return mAllRedirectsSameOrigin; }
+
+ // If this is false the values of redirectStart/End will be 0 This is false if
+ // no redirects occured, or if any of the responses failed the
+ // timing-allow-origin check in HttpBaseChannel::TimingAllowCheck
+ //
+ // If aEnsureSameOriginAndIgnoreTAO is false, it checks if all redirects pass
+ // TAO. When it is true, it checks if all redirects are same-origin and
+ // ignores the result of TAO.
+ bool ShouldReportCrossOriginRedirect(
+ bool aEnsureSameOriginAndIgnoreTAO) const;
+
+ // Cached result of CheckAllowedOrigin. If false, security sensitive
+ // attributes of the resourceTiming object will be set to 0
+ bool TimingAllowed() const { return mTimingAllowed; }
+
+ nsTArray<nsCOMPtr<nsIServerTiming>> GetServerTiming();
+
+ private:
+ // Checks if the resource is either same origin as the page that started
+ // the load, or if the response contains the Timing-Allow-Origin header
+ // with a value of * or matching the domain of the loading Principal
+ bool CheckAllowedOrigin(nsIHttpChannel* aResourceChannel,
+ nsITimedChannel* aChannel);
+
+ nsTArray<nsCOMPtr<nsIServerTiming>> mServerTiming;
+ nsString mNextHopProtocol;
+
+ TimeStamp mAsyncOpen;
+ TimeStamp mRedirectStart;
+ TimeStamp mRedirectEnd;
+ TimeStamp mDomainLookupStart;
+ TimeStamp mDomainLookupEnd;
+ TimeStamp mConnectStart;
+ TimeStamp mSecureConnectionStart;
+ TimeStamp mConnectEnd;
+ TimeStamp mRequestStart;
+ TimeStamp mResponseStart;
+ TimeStamp mCacheReadStart;
+ TimeStamp mResponseEnd;
+ TimeStamp mCacheReadEnd;
+
+ // ServiceWorker interception timing information
+ TimeStamp mWorkerStart;
+ TimeStamp mWorkerRequestStart;
+ TimeStamp mWorkerResponseEnd;
+
+ // This is an offset that will be added to each timing ([ms] resolution).
+ // There are only 2 possible values: (1) logicaly equal to navigationStart
+ // TimeStamp (results are absolute timstamps - wallclock); (2) "0" (results
+ // are relative to the navigation start).
+ DOMHighResTimeStamp mZeroTime = 0;
+
+ DOMHighResTimeStamp mFetchStart = 0;
+
+ uint64_t mEncodedBodySize = 0;
+ uint64_t mTransferSize = 0;
+ uint64_t mDecodedBodySize = 0;
+
+ uint8_t mRedirectCount = 0;
+
+ bool mAllRedirectsSameOrigin = false;
+
+ bool mAllRedirectsPassTAO = false;
+
+ bool mSecureConnection = false;
+
+ bool mTimingAllowed = false;
+
+ bool mInitialized = false;
+};
+
+// Script "performance.timing" object
+class PerformanceTiming final : public nsWrapperCache {
+ public:
+ /**
+ * @param aPerformance
+ * The performance object (the JS parent).
+ * This will allow access to "window.performance.timing" attribute
+ * for the navigation timing (can't be null).
+ * @param aChannel
+ * An nsITimedChannel used to gather all the networking timings by
+ * both the navigation timing and the resource timing (can't be null).
+ * @param aHttpChannel
+ * An nsIHttpChannel (the resource's http channel).
+ * This will be used by the resource timing cross-domain check
+ * algorithm.
+ * Argument is null for the navigation timing (navigation timing uses
+ * another algorithm for the cross-domain redirects).
+ * @param aZeroTime
+ * The offset that will be added to the timestamp of each event. This
+ * argument should be equal to performance.navigationStart for
+ * navigation timing and "0" for the resource timing.
+ */
+ PerformanceTiming(Performance* aPerformance, nsITimedChannel* aChannel,
+ nsIHttpChannel* aHttpChannel,
+ DOMHighResTimeStamp aZeroTime);
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PerformanceTiming)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(PerformanceTiming)
+
+ nsDOMNavigationTiming* GetDOMTiming() const {
+ return mPerformance->GetDOMTiming();
+ }
+
+ Performance* GetParentObject() const { return mPerformance; }
+
+ virtual JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // PerformanceNavigation WebIDL methods
+ DOMTimeMilliSec NavigationStart() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetNavigationStart(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec UnloadEventStart() {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetUnloadEventStart(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec UnloadEventEnd() {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetUnloadEventEnd(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ // Low resolution (used by navigation timing)
+ DOMTimeMilliSec FetchStart();
+ DOMTimeMilliSec RedirectStart();
+ DOMTimeMilliSec RedirectEnd();
+ DOMTimeMilliSec DomainLookupStart();
+ DOMTimeMilliSec DomainLookupEnd();
+ DOMTimeMilliSec ConnectStart();
+ DOMTimeMilliSec SecureConnectionStart();
+ DOMTimeMilliSec ConnectEnd();
+ DOMTimeMilliSec RequestStart();
+ DOMTimeMilliSec ResponseStart();
+ DOMTimeMilliSec ResponseEnd();
+
+ DOMTimeMilliSec DomLoading() {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetDomLoading(), mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec DomInteractive() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetDomInteractive(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec DomContentLoadedEventStart() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetDomContentLoadedEventStart(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec DomContentLoadedEventEnd() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetDomContentLoadedEventEnd(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec DomComplete() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetDomComplete(), mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec LoadEventStart() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetLoadEventStart(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec LoadEventEnd() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetLoadEventEnd(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec TimeToNonBlankPaint() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetTimeToNonBlankPaint(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec TimeToContentfulPaint() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetTimeToContentfulComposite(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec TimeToDOMContentFlushed() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetTimeToDOMContentFlushed(),
+ mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ DOMTimeMilliSec TimeToFirstInteractive() const {
+ if (!StaticPrefs::dom_enable_performance()) {
+ return 0;
+ }
+ return nsRFPService::ReduceTimePrecisionAsMSecs(
+ GetDOMTiming()->GetTimeToTTFI(), mPerformance->GetRandomTimelineSeed(),
+ mPerformance->GetRTPCallerType());
+ }
+
+ PerformanceTimingData* Data() const { return mTimingData.get(); }
+
+ private:
+ ~PerformanceTiming();
+
+ bool IsTopLevelContentDocument() const;
+
+ RefPtr<Performance> mPerformance;
+
+ UniquePtr<PerformanceTimingData> mTimingData;
+};
+
+} // namespace mozilla::dom
+
+namespace mozilla::ipc {
+
+template <>
+struct IPDLParamTraits<mozilla::dom::PerformanceTimingData> {
+ using paramType = mozilla::dom::PerformanceTimingData;
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ const paramType& aParam) {
+ WriteIPDLParam(aWriter, aActor, aParam.mServerTiming);
+ WriteIPDLParam(aWriter, aActor, aParam.mNextHopProtocol);
+ WriteIPDLParam(aWriter, aActor, aParam.mAsyncOpen);
+ WriteIPDLParam(aWriter, aActor, aParam.mRedirectStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mRedirectEnd);
+ WriteIPDLParam(aWriter, aActor, aParam.mDomainLookupStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mDomainLookupEnd);
+ WriteIPDLParam(aWriter, aActor, aParam.mConnectStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mSecureConnectionStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mConnectEnd);
+ WriteIPDLParam(aWriter, aActor, aParam.mRequestStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mResponseStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mCacheReadStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mResponseEnd);
+ WriteIPDLParam(aWriter, aActor, aParam.mCacheReadEnd);
+ WriteIPDLParam(aWriter, aActor, aParam.mWorkerStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mWorkerRequestStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mWorkerResponseEnd);
+ WriteIPDLParam(aWriter, aActor, aParam.mZeroTime);
+ WriteIPDLParam(aWriter, aActor, aParam.mFetchStart);
+ WriteIPDLParam(aWriter, aActor, aParam.mEncodedBodySize);
+ WriteIPDLParam(aWriter, aActor, aParam.mTransferSize);
+ WriteIPDLParam(aWriter, aActor, aParam.mDecodedBodySize);
+ WriteIPDLParam(aWriter, aActor, aParam.mRedirectCount);
+ WriteIPDLParam(aWriter, aActor, aParam.mAllRedirectsSameOrigin);
+ WriteIPDLParam(aWriter, aActor, aParam.mAllRedirectsPassTAO);
+ WriteIPDLParam(aWriter, aActor, aParam.mSecureConnection);
+ WriteIPDLParam(aWriter, aActor, aParam.mTimingAllowed);
+ WriteIPDLParam(aWriter, aActor, aParam.mInitialized);
+ }
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ paramType* aResult) {
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mServerTiming)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mNextHopProtocol)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mAsyncOpen)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mRedirectStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mRedirectEnd)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mDomainLookupStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mDomainLookupEnd)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mConnectStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mSecureConnectionStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mConnectEnd)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mRequestStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mResponseStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mCacheReadStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mResponseEnd)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mCacheReadEnd)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mWorkerStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mWorkerRequestStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mWorkerResponseEnd)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mZeroTime)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mFetchStart)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mEncodedBodySize)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mTransferSize)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mDecodedBodySize)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mRedirectCount)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mAllRedirectsSameOrigin)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mAllRedirectsPassTAO)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mSecureConnection)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mTimingAllowed)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mInitialized)) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct IPDLParamTraits<nsIServerTiming*> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ nsIServerTiming* aParam) {
+ nsAutoCString name;
+ Unused << aParam->GetName(name);
+ double duration = 0;
+ Unused << aParam->GetDuration(&duration);
+ nsAutoCString description;
+ Unused << aParam->GetDescription(description);
+ WriteIPDLParam(aWriter, aActor, name);
+ WriteIPDLParam(aWriter, aActor, duration);
+ WriteIPDLParam(aWriter, aActor, description);
+ }
+
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ RefPtr<nsIServerTiming>* aResult) {
+ nsAutoCString name;
+ double duration;
+ nsAutoCString description;
+ if (!ReadIPDLParam(aReader, aActor, &name)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &duration)) {
+ return false;
+ }
+ if (!ReadIPDLParam(aReader, aActor, &description)) {
+ return false;
+ }
+
+ RefPtr<nsServerTiming> timing = new nsServerTiming();
+ timing->SetName(name);
+ timing->SetDuration(duration);
+ timing->SetDescription(description);
+ *aResult = timing.forget();
+ return true;
+ }
+};
+
+} // namespace mozilla::ipc
+
+#endif // mozilla_dom_PerformanceTiming_h
diff --git a/dom/performance/PerformanceTimingTypes.ipdlh b/dom/performance/PerformanceTimingTypes.ipdlh
new file mode 100644
index 0000000000..fbbdf44636
--- /dev/null
+++ b/dom/performance/PerformanceTimingTypes.ipdlh
@@ -0,0 +1,50 @@
+/* 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/. */
+
+using class mozilla::TimeStamp from "mozilla/TimeStamp.h";
+using DOMHighResTimeStamp from "nsDOMNavigationTiming.h";
+
+namespace mozilla {
+namespace dom {
+
+struct IPCServerTiming {
+ nsCString name;
+ double duration;
+ nsCString description;
+};
+
+struct IPCPerformanceTimingData {
+ IPCServerTiming[] serverTiming;
+ nsString nextHopProtocol;
+ TimeStamp asyncOpen;
+ TimeStamp redirectStart;
+ TimeStamp redirectEnd;
+ TimeStamp domainLookupStart;
+ TimeStamp domainLookupEnd;
+ TimeStamp connectStart;
+ TimeStamp secureConnectionStart;
+ TimeStamp connectEnd;
+ TimeStamp requestStart;
+ TimeStamp responseStart;
+ TimeStamp cacheReadStart;
+ TimeStamp responseEnd;
+ TimeStamp cacheReadEnd;
+ TimeStamp workerStart;
+ TimeStamp workerRequestStart;
+ TimeStamp workerResponseEnd;
+ DOMHighResTimeStamp zeroTime;
+ DOMHighResTimeStamp fetchStart;
+ uint64_t encodedBodySize;
+ uint64_t transferSize;
+ uint64_t decodedBodySize;
+ uint8_t redirectCount;
+ bool allRedirectsSameOrigin;
+ bool allRedirectsPassTAO;
+ bool secureConnection;
+ bool timingAllowed;
+ bool initialized;
+};
+
+}
+}
diff --git a/dom/performance/PerformanceWorker.cpp b/dom/performance/PerformanceWorker.cpp
new file mode 100644
index 0000000000..1c77910a78
--- /dev/null
+++ b/dom/performance/PerformanceWorker.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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 "PerformanceWorker.h"
+#include "mozilla/dom/WorkerScope.h"
+#include "mozilla/StaticPrefs_dom.h"
+
+namespace mozilla::dom {
+
+PerformanceWorker::PerformanceWorker(WorkerGlobalScope* aGlobalScope)
+ : Performance(aGlobalScope) {
+ MOZ_ASSERT(aGlobalScope);
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+ workerPrivate->AssertIsOnWorkerThread();
+}
+
+PerformanceWorker::~PerformanceWorker() {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ if (workerPrivate) {
+ workerPrivate->AssertIsOnWorkerThread();
+ }
+}
+
+void PerformanceWorker::InsertUserEntry(PerformanceEntry* aEntry) {
+ if (StaticPrefs::dom_performance_enable_user_timing_logging()) {
+ nsAutoCString uri;
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+ nsCOMPtr<nsIURI> scriptURI = workerPrivate->GetResolvedScriptURI();
+ if (!scriptURI || NS_FAILED(scriptURI->GetHost(uri))) {
+ // If we have no URI, just put in "none".
+ uri.AssignLiteral("none");
+ }
+ Performance::LogEntry(aEntry, uri);
+ }
+ Performance::InsertUserEntry(aEntry);
+}
+
+TimeStamp PerformanceWorker::CreationTimeStamp() const {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+ return workerPrivate->CreationTimeStamp();
+}
+
+DOMHighResTimeStamp PerformanceWorker::CreationTime() const {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+ return workerPrivate->CreationTime();
+}
+
+uint64_t PerformanceWorker::GetRandomTimelineSeed() {
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_DIAGNOSTIC_ASSERT(workerPrivate);
+ return workerPrivate->GetRandomTimelineSeed();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/performance/PerformanceWorker.h b/dom/performance/PerformanceWorker.h
new file mode 100644
index 0000000000..f0539aa02d
--- /dev/null
+++ b/dom/performance/PerformanceWorker.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef mozilla_dom_PerformanceWorker_h
+#define mozilla_dom_PerformanceWorker_h
+
+#include "Performance.h"
+
+namespace mozilla::dom {
+
+class WorkerGlobalScope;
+
+class PerformanceWorker final : public Performance {
+ public:
+ explicit PerformanceWorker(WorkerGlobalScope* aGlobalScope);
+
+ PerformanceStorage* AsPerformanceStorage() override {
+ MOZ_CRASH("This should not be called on workers.");
+ return nullptr;
+ }
+
+ virtual PerformanceTiming* Timing() override {
+ MOZ_CRASH("This should not be called on workers.");
+ return nullptr;
+ }
+
+ virtual PerformanceNavigation* Navigation() override {
+ MOZ_CRASH("This should not be called on workers.");
+ return nullptr;
+ }
+
+ virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) override {
+ MOZ_CRASH("This should not be called on workers.");
+ }
+
+ TimeStamp CreationTimeStamp() const override;
+
+ DOMHighResTimeStamp CreationTime() const override;
+
+ virtual void GetMozMemory(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aObj) override {
+ MOZ_CRASH("This should not be called on workers.");
+ }
+
+ virtual nsDOMNavigationTiming* GetDOMTiming() const override {
+ MOZ_CRASH("This should not be called on workers.");
+ return nullptr;
+ }
+
+ virtual uint64_t GetRandomTimelineSeed() override;
+
+ virtual nsITimedChannel* GetChannel() const override {
+ MOZ_CRASH("This should not be called on workers.");
+ return nullptr;
+ }
+
+ void QueueNavigationTimingEntry() override {
+ MOZ_CRASH("This should not be called on workers.");
+ }
+
+ void UpdateNavigationTimingEntry() override {
+ MOZ_CRASH("This should not be called on workers.");
+ }
+
+ void InsertEventTimingEntry(PerformanceEventTiming*) override {
+ MOZ_CRASH("This should not be called on workers.");
+ }
+
+ void BufferEventTimingEntryIfNeeded(PerformanceEventTiming*) override {
+ MOZ_CRASH("This should not be called on workers.");
+ }
+
+ void DispatchPendingEventTimingEntries() override {
+ MOZ_CRASH("This should not be called on workders.");
+ }
+
+ class EventCounts* EventCounts() override {
+ MOZ_CRASH("This should not be called on workers");
+ }
+
+ protected:
+ ~PerformanceWorker();
+
+ void InsertUserEntry(PerformanceEntry* aEntry) override;
+
+ void DispatchBufferFullEvent() override {
+ // Nothing to do here. See bug 1432758.
+ }
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_PerformanceWorker_h
diff --git a/dom/performance/metrics.yaml b/dom/performance/metrics.yaml
new file mode 100644
index 0000000000..995afc1024
--- /dev/null
+++ b/dom/performance/metrics.yaml
@@ -0,0 +1,32 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: DOM: Performance'
+
+performance.time:
+ response_start:
+ type: timing_distribution
+ time_unit: millisecond
+ telemetry_mirror: TIME_TO_RESPONSE_START_MS
+ description: >
+ Time from navigationStart to responseStart as per the W3C
+ Performance Timing API.
+ (Migrated from the geckoview metric of the same name.)
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1344893
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1489524
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1877842
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1580077#c10
+ notification_emails:
+ - vchin@mozilla.com
+ - perf-telemetry-alerts@mozilla.com
+ expires: never
diff --git a/dom/performance/moz.build b/dom/performance/moz.build
new file mode 100644
index 0000000000..8423932247
--- /dev/null
+++ b/dom/performance/moz.build
@@ -0,0 +1,65 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Performance")
+
+EXPORTS.mozilla.dom += [
+ "EventCounts.h",
+ "LargestContentfulPaint.h",
+ "Performance.h",
+ "PerformanceEntry.h",
+ "PerformanceEventTiming.h",
+ "PerformanceMainThread.h",
+ "PerformanceMark.h",
+ "PerformanceMeasure.h",
+ "PerformanceNavigation.h",
+ "PerformanceNavigationTiming.h",
+ "PerformanceObserver.h",
+ "PerformanceObserverEntryList.h",
+ "PerformancePaintTiming.h",
+ "PerformanceResourceTiming.h",
+ "PerformanceServerTiming.h",
+ "PerformanceService.h",
+ "PerformanceStorage.h",
+ "PerformanceStorageWorker.h",
+ "PerformanceTiming.h",
+ "PerformanceWorker.h",
+]
+
+UNIFIED_SOURCES += [
+ "EventCounts.cpp",
+ "LargestContentfulPaint.cpp",
+ "Performance.cpp",
+ "PerformanceEntry.cpp",
+ "PerformanceEventTiming.cpp",
+ "PerformanceMainThread.cpp",
+ "PerformanceMark.cpp",
+ "PerformanceMeasure.cpp",
+ "PerformanceNavigation.cpp",
+ "PerformanceNavigationTiming.cpp",
+ "PerformanceObserver.cpp",
+ "PerformanceObserverEntryList.cpp",
+ "PerformancePaintTiming.cpp",
+ "PerformanceResourceTiming.cpp",
+ "PerformanceServerTiming.cpp",
+ "PerformanceService.cpp",
+ "PerformanceStorageWorker.cpp",
+ "PerformanceTiming.cpp",
+ "PerformanceWorker.cpp",
+]
+
+IPDL_SOURCES += [
+ "PerformanceTimingTypes.ipdlh",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+MOCHITEST_MANIFESTS += ["tests/mochitest.toml"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/performance/tests/empty.js b/dom/performance/tests/empty.js
new file mode 100644
index 0000000000..3b44754e30
--- /dev/null
+++ b/dom/performance/tests/empty.js
@@ -0,0 +1 @@
+/* Nothing here */
diff --git a/dom/performance/tests/logo.png b/dom/performance/tests/logo.png
new file mode 100644
index 0000000000..a05926bcd7
--- /dev/null
+++ b/dom/performance/tests/logo.png
Binary files differ
diff --git a/dom/performance/tests/mochitest.toml b/dom/performance/tests/mochitest.toml
new file mode 100644
index 0000000000..198f8eb542
--- /dev/null
+++ b/dom/performance/tests/mochitest.toml
@@ -0,0 +1,67 @@
+[DEFAULT]
+support-files = [
+ "test_performance_observer.js",
+ "test_performance_user_timing.js",
+ "test_worker_performance_now.js",
+ "worker_performance_user_timing.js",
+ "worker_performance_observer.js",
+ "sharedworker_performance_user_timing.js",
+ "test_worker_performance_entries.js",
+ "test_worker_performance_entries.sjs",
+ "empty.js",
+ "serverTiming.sjs",
+]
+
+["test_performance_navigation_timing.html"]
+
+["test_performance_observer.html"]
+
+["test_performance_paint_observer.html"]
+support-files = [
+ "test_performance_paint_observer_helper.html",
+ "logo.png",
+]
+
+["test_performance_paint_timing.html"]
+support-files = [
+ "test_performance_paint_timing_helper.html",
+ "logo.png",
+]
+
+["test_performance_server_timing.html"]
+scheme = "https"
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_performance_server_timing_plain_http.html"]
+skip-if = [
+ "http3",
+ "http2",
+]
+
+["test_performance_timing_json.html"]
+
+["test_performance_user_timing.html"]
+
+["test_performance_user_timing_dying_global.html"]
+
+["test_sharedWorker_performance_user_timing.html"]
+skip-if = ["true"] # Bug 1571904
+
+["test_timeOrigin.html"]
+skip-if = ["os == 'android'"] # Bug 1525959
+
+["test_worker_observer.html"]
+
+["test_worker_performance_entries.html"]
+skip-if = [
+ "os == 'android'", # Bug 1525959
+ "http3",
+ "http2",
+]
+
+["test_worker_performance_now.html"]
+
+["test_worker_user_timing.html"]
diff --git a/dom/performance/tests/serverTiming.sjs b/dom/performance/tests/serverTiming.sjs
new file mode 100644
index 0000000000..8a93829fa5
--- /dev/null
+++ b/dom/performance/tests/serverTiming.sjs
@@ -0,0 +1,41 @@
+var responseServerTiming = [
+ { metric: "metric1", duration: "123.4", description: "description1" },
+ { metric: "metric2", duration: "456.78", description: "description2" },
+];
+var trailerServerTiming = [
+ { metric: "metric3", duration: "789.11", description: "description3" },
+ { metric: "metric4", duration: "1112.13", description: "description4" },
+];
+
+function createServerTimingHeader(headerData) {
+ var header = "";
+ for (var i = 0; i < headerData.length; i++) {
+ header +=
+ "Server-Timing:" +
+ headerData[i].metric +
+ ";" +
+ "dur=" +
+ headerData[i].duration +
+ ";" +
+ "desc=" +
+ headerData[i].description +
+ "\r\n";
+ }
+ return header;
+}
+
+function handleRequest(request, response) {
+ var body = "c\r\ndata reached\r\n3\r\nhej\r\n0\r\n";
+
+ response.seizePower();
+ response.write("HTTP/1.1 200 OK\r\n");
+ response.write("Content-Type: text/plain\r\n");
+ response.write(createServerTimingHeader(responseServerTiming));
+
+ response.write("Transfer-Encoding: chunked\r\n");
+ response.write("\r\n");
+ response.write(body);
+ response.write(createServerTimingHeader(trailerServerTiming));
+ response.write("\r\n");
+ response.finish();
+}
diff --git a/dom/performance/tests/sharedworker_performance_user_timing.js b/dom/performance/tests/sharedworker_performance_user_timing.js
new file mode 100644
index 0000000000..6dcbd5d7d9
--- /dev/null
+++ b/dom/performance/tests/sharedworker_performance_user_timing.js
@@ -0,0 +1,38 @@
+var port;
+
+function ok(a, msg) {
+ dump("OK: " + !!a + " => " + a + " " + msg + "\n");
+ port.postMessage({ type: "status", status: !!a, msg: a + ": " + msg });
+}
+
+function is(a, b, msg) {
+ dump("IS: " + (a === b) + " => " + a + " | " + b + " " + msg + "\n");
+ port.postMessage({
+ type: "status",
+ status: a === b,
+ msg: a + " === " + b + ": " + msg,
+ });
+}
+
+function isnot(a, b, msg) {
+ dump("ISNOT: " + (a === b) + " => " + a + " | " + b + " " + msg + "\n");
+ port.postMessage({
+ type: "status",
+ status: a != b,
+ msg: a + " != " + b + ": " + msg,
+ });
+}
+
+importScripts("test_performance_user_timing.js");
+
+onconnect = function (evt) {
+ port = evt.ports[0];
+
+ for (var i = 0; i < steps.length; ++i) {
+ performance.clearMarks();
+ performance.clearMeasures();
+ steps[i]();
+ }
+
+ port.postMessage({ type: "finish" });
+};
diff --git a/dom/performance/tests/test_performance_navigation_timing.html b/dom/performance/tests/test_performance_navigation_timing.html
new file mode 100644
index 0000000000..abcf9fd340
--- /dev/null
+++ b/dom/performance/tests/test_performance_navigation_timing.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1462891
+ -->
+ <head>
+ <title>Test for Bug 1462891</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1462891">Mozilla Bug 1462891 - Navigation Timing API</a>
+ <div id="content">
+ </div>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var index = 0;
+ let isRounded = (x, shouldRound, expectedPrecision) => {
+ if (!shouldRound)
+ return true;
+
+ let rounded = (Math.floor(x / expectedPrecision) * expectedPrecision);
+ // First we do the perfectly normal check that should work just fine
+ if (rounded === x || x === 0)
+ return true;
+
+ // When we're diving by non-whole numbers, we may not get perfect
+ // multiplication/division because of floating points.
+ // When dealing with ms since epoch, a double's precision is on the order
+ // of 1/5 of a microsecond, so we use a value a little higher than that as
+ // our epsilon.
+ // To be clear, this error is introduced in our re-calculation of 'rounded'
+ // above in JavaScript.
+ if (Math.abs(rounded - x + expectedPrecision) < .0005) {
+ return true;
+ } else if (Math.abs(rounded - x) < .0005) {
+ return true;
+ }
+
+ // Then we handle the case where you're sub-millisecond and the timer is not
+ // We check that the timer is not sub-millisecond by assuming it is not if it
+ // returns an even number of milliseconds
+ if (expectedPrecision < 1 && Math.round(x) == x) {
+ if (Math.round(rounded) == x) {
+ return true;
+ }
+ }
+
+ ok(false, "Looming Test Failure, Additional Debugging Info: Expected Precision: " + expectedPrecision + " Measured Value: " + x +
+ " Rounded Vaue: " + rounded + " Fuzzy1: " + Math.abs(rounded - x + expectedPrecision) +
+ " Fuzzy 2: " + Math.abs(rounded - x));
+
+ return false;
+ };
+
+ var metrics = [
+ "unloadEventStart",
+ "unloadEventEnd",
+ "domInteractive",
+ "domContentLoadedEventStart",
+ "domContentLoadedEventEnd",
+ "domComplete",
+ "loadEventStart",
+ "loadEventEnd"
+ ];
+
+ async function runTests(resistFingerprinting, reduceTimerPrecision, expectedPrecision) {
+ await SpecialPowers.pushPrefEnv({
+ "set": [["privacy.resistFingerprinting", resistFingerprinting],
+ ["privacy.reduceTimerPrecision", reduceTimerPrecision],
+ ["privacy.resistFingerprinting.reduceTimerPrecision.microseconds", expectedPrecision * 1000]
+ ]});
+ var entries = performance.getEntriesByType("navigation");
+ is(entries.length, 1, "Checking PerformanceNavigationEntry count");
+
+ for (let i=0; i<entries.length; i++) {
+ for (let j=0; j<metrics.length; j++) {
+ ok(isRounded(entries[i][metrics[j]], reduceTimerPrecision, expectedPrecision),
+ "Testing " + metrics[j] + " with value " + entries[i][metrics[j]] +
+ " with resistFingerprinting " + resistFingerprinting + " reduceTimerPrecision " +
+ reduceTimerPrecision + " precision " + expectedPrecision);
+ }
+ }
+ }
+
+ async function startTests() {
+ await runTests(false, false, 2);
+ await runTests(true, false, 2);
+ await runTests(true, true, 2);
+ await runTests(false, true, 1000);
+ await runTests(false, true, 133);
+ await runTests(false, true, 13);
+ await runTests(false, true, 2);
+ await runTests(false, true, 1);
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(startTests);
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_performance_observer.html b/dom/performance/tests/test_performance_observer.html
new file mode 100644
index 0000000000..86c780c56c
--- /dev/null
+++ b/dom/performance/tests/test_performance_observer.html
@@ -0,0 +1,142 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Test for performance observer</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="log"></div>
+<script>
+SimpleTest.requestFlakyTimeout("For testing when observer callbacks should not be called.");
+SimpleTest.waitForExplicitFinish();
+
+let _tests = [];
+
+let test = promise_test = fn => {
+ let cleanups = [];
+ _tests.push(async () => {
+ try {
+ await fn({
+ add_cleanup: f => { cleanups.push(f); },
+ step_timeout(f, timeout) {
+ var test_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(() => {
+ return f.apply(test_this, args);
+ }, timeout);
+ }
+ });
+ } catch(e) {
+ ok(false, `got unexpected exception ${e}`);
+ }
+ try {
+ for (const f of cleanups) {
+ f();
+ }
+ runNextTest();
+ } catch (e) {
+ ok(false, `got unexpected exception during cleanup ${e}`);
+ }
+ });
+}
+
+function runNextTest() {
+ if (!_tests.length) {
+ SimpleTest.finish()
+ return;
+ }
+ _tests.shift()();
+}
+
+function assert_equals(actual, expected, description) {
+ ok(typeof actual == typeof expected,
+ `${description} expected (${typeof expected}) ${expected} but got (${typeof actual}) ${actual}`);
+ ok(Object.is(actual, expected),
+ `${description} expected ${expected} but got ${actual}`);
+}
+
+function assert_array_equals(actual, expected, description) {
+ ok(actual.length === expected.length,
+ `${description} lengths differ, expected ${expected.length} but got ${actual.length}`);
+ for (var i = 0; i < actual.length; i++) {
+ ok(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ `${description} property expected to be ${expected[i]} but got ${actual[i]}`);
+ }
+}
+
+function assert_throws(expected_exc, func, desc) {
+ try {
+ func.call(this);
+ } catch(e) {
+ var actual = e.name || e.type;
+ var expected = expected_exc.name || expected_exc.type;
+ ok(actual == expected,
+ `Expected '${expected}', got '${actual}'.`);
+ return;
+ }
+ ok(false, "Expected exception, but none was thrown");
+}
+
+function assert_unreached(description) {
+ ok(false, `${description} reached unreachable code`);
+}
+</script>
+<script src="test_performance_observer.js"></script>
+<script>
+function makeXHR(aUrl) {
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open("get", aUrl, true);
+ xmlhttp.send();
+}
+
+let waitForConsole = new Promise(resolve => {
+ SimpleTest.monitorConsole(resolve, [{
+ message: /JavaScript Warning: "Ignoring unsupported entryTypes: invalid."/,
+ }]);
+});
+
+promise_test(t => {
+ var promise = new Promise(resolve => {
+ performance.clearResourceTimings();
+
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe({entryTypes: ['resource']});
+ t.add_cleanup(() => observer.disconnect());
+ });
+
+ makeXHR("test-data.json");
+
+ return promise.then(async list => {
+ assert_equals(list.getEntries().length, 1);
+ assert_array_equals(list.getEntries(),
+ performance.getEntriesByType("resource"),
+ "Observed 'resource' entries should equal to entries obtained by getEntriesByType.");
+
+ // getEntries filtering tests
+ assert_array_equals(list.getEntries({name: "http://mochi.test:8888/tests/dom/base/test/test-data.json"}),
+ performance.getEntriesByName("http://mochi.test:8888/tests/dom/base/test/test-data.json"),
+ "getEntries with name filter should return correct results.");
+ assert_array_equals(list.getEntries({entryType: "resource"}),
+ performance.getEntriesByType("resource"),
+ "getEntries with entryType filter should return correct results.");
+ assert_array_equals(list.getEntries({initiatorType: "xmlhttprequest"}),
+ performance.getEntriesByType("resource"),
+ "getEntries with initiatorType filter should return correct results.");
+ assert_array_equals(list.getEntries({initiatorType: "link"}),
+ [],
+ "getEntries with non-existent initiatorType filter should return an empty array.");
+
+ SimpleTest.endMonitorConsole();
+ await waitForConsole;
+ });
+}, "resource-timing test");
+
+runNextTest();
+</script>
+</body>
diff --git a/dom/performance/tests/test_performance_observer.js b/dom/performance/tests/test_performance_observer.js
new file mode 100644
index 0000000000..ddc57d4096
--- /dev/null
+++ b/dom/performance/tests/test_performance_observer.js
@@ -0,0 +1,286 @@
+test(t => {
+ assert_throws(
+ { name: "TypeError" },
+ function () {
+ new PerformanceObserver();
+ },
+ "PerformanceObserver constructor should throw TypeError if no argument is specified."
+ );
+
+ assert_throws(
+ { name: "TypeError" },
+ function () {
+ new PerformanceObserver({});
+ },
+ "PerformanceObserver constructor should throw TypeError if the argument is not a function."
+ );
+}, "Test that PerformanceObserver constructor throws exception");
+
+test(t => {
+ var observer = new PerformanceObserver(() => {});
+
+ assert_throws(
+ { name: "TypeError" },
+ function () {
+ observer.observe();
+ },
+ "observe() should throw TypeError exception if no option specified."
+ );
+
+ assert_throws(
+ { name: "TypeError" },
+ function () {
+ observer.observe({ unsupportedAttribute: "unsupported" });
+ },
+ "obsrve() should throw TypeError exception if the option has no 'entryTypes' attribute."
+ );
+
+ assert_equals(
+ undefined,
+ observer.observe({ entryTypes: [] }),
+ "observe() should silently ignore empty 'entryTypes' sequence."
+ );
+
+ assert_throws(
+ { name: "TypeError" },
+ function () {
+ observer.observe({ entryTypes: null });
+ },
+ "obsrve() should throw TypeError exception if 'entryTypes' attribute is null."
+ );
+
+ assert_equals(
+ undefined,
+ observer.observe({ entryTypes: ["invalid"] }),
+ "observe() should silently ignore invalid 'entryTypes' values."
+ );
+}, "Test that PerformanceObserver.observe throws exception");
+
+function promiseObserve(test, options) {
+ return new Promise(resolve => {
+ performance.clearMarks();
+ performance.clearMeasures();
+
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe(options);
+ test.add_cleanup(() => observer.disconnect());
+ });
+}
+
+promise_test(t => {
+ var promise = promiseObserve(t, { entryTypes: ["mark", "measure"] });
+
+ performance.mark("test-start");
+ performance.mark("test-end");
+ performance.measure("test-measure", "test-start", "test-end");
+
+ return promise.then(list => {
+ assert_equals(
+ list.getEntries().length,
+ 3,
+ "There should be three observed entries."
+ );
+
+ var markEntries = list.getEntries().filter(entry => {
+ return entry.entryType == "mark";
+ });
+ assert_array_equals(
+ markEntries,
+ performance.getEntriesByType("mark"),
+ "Observed 'mark' entries should equal to entries obtained by getEntriesByType."
+ );
+
+ var measureEntries = list.getEntries().filter(entry => {
+ return entry.entryType == "measure";
+ });
+ assert_array_equals(
+ measureEntries,
+ performance.getEntriesByType("measure"),
+ "Observed 'measure' entries should equal to entries obtained by getEntriesByType."
+ );
+ });
+}, "Test for user-timing with PerformanceObserver");
+
+promise_test(t => {
+ var promise = new Promise((resolve, reject) => {
+ performance.clearMarks();
+ performance.clearMeasures();
+
+ var observer = new PerformanceObserver(list => reject(list));
+ observer.observe({ entryTypes: ["mark", "measure"] });
+ observer.disconnect();
+ t.step_timeout(resolve, 100);
+ });
+
+ performance.mark("test-start");
+ performance.mark("test-end");
+ performance.measure("test-measure", "test-start", "test-end");
+
+ return promise.then(
+ () => {
+ assert_equals(performance.getEntriesByType("mark").length, 2);
+ assert_equals(performance.getEntriesByType("measure").length, 1);
+ },
+ list => {
+ assert_unreached("Observer callback should never be called.");
+ }
+ );
+}, "Nothing should be notified after disconnecting observer");
+
+promise_test(t => {
+ var promise = promiseObserve(t, { entryTypes: ["mark"] });
+
+ performance.mark("test");
+
+ return promise.then(list => {
+ assert_array_equals(
+ list.getEntries({ entryType: "mark" }),
+ performance.getEntriesByType("mark"),
+ "getEntries with entryType filter should return correct results."
+ );
+
+ assert_array_equals(
+ list.getEntries({ name: "test" }),
+ performance.getEntriesByName("test"),
+ "getEntries with name filter should return correct results."
+ );
+
+ assert_array_equals(
+ list.getEntries({ name: "test", entryType: "mark" }),
+ performance.getEntriesByName("test"),
+ "getEntries with name and entryType filter should return correct results."
+ );
+
+ assert_array_equals(
+ list.getEntries({ name: "invalid" }),
+ [],
+ "getEntries with non-existent name filter should return an empty array."
+ );
+
+ assert_array_equals(
+ list.getEntries({ name: "test", entryType: "measure" }),
+ [],
+ "getEntries with name filter and non-existent entryType should return an empty array."
+ );
+
+ assert_array_equals(
+ list.getEntries({ name: "invalid", entryType: "mark" }),
+ [],
+ "getEntries with non-existent name and entryType filter should return an empty array."
+ );
+
+ assert_array_equals(
+ list.getEntries({ initiatorType: "xmlhttprequest" }),
+ [],
+ "getEntries with initiatorType filter should return an empty array."
+ );
+ });
+}, "Test for PerformanceObserverEntryList.getEntries");
+
+promise_test(t => {
+ var promise = promiseObserve(t, { entryTypes: ["mark", "measure"] });
+
+ performance.mark("test");
+ performance.measure("test-measure", "test", "test");
+
+ return promise.then(list => {
+ assert_array_equals(
+ list.getEntriesByType("mark"),
+ performance.getEntriesByType("mark")
+ );
+ assert_array_equals(
+ list.getEntriesByType("measure"),
+ performance.getEntriesByType("measure")
+ );
+ });
+}, "Test for PerformanceObserverEntryList.getEntriesByType");
+
+promise_test(t => {
+ var promise = promiseObserve(t, { entryTypes: ["mark", "measure"] });
+
+ performance.mark("test");
+ performance.measure("test-measure", "test", "test");
+
+ return promise.then(list => {
+ assert_array_equals(
+ list.getEntriesByName("test"),
+ performance.getEntriesByName("test")
+ );
+ assert_array_equals(
+ list.getEntriesByName("test-measure"),
+ performance.getEntriesByName("test-measure")
+ );
+ });
+}, "Test for PerformanceObserverEntryList.getEntriesByName");
+
+promise_test(t => {
+ var promise = new Promise(resolve => {
+ performance.clearMarks();
+ performance.clearMeasures();
+
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe({ entryTypes: ["mark", "measure"] });
+ observer.observe({ entryTypes: ["mark", "measure"] });
+ t.add_cleanup(() => observer.disconnect());
+ });
+
+ performance.mark("test-start");
+ performance.mark("test-end");
+ performance.measure("test-measure", "test-start", "test-end");
+
+ return promise.then(list => {
+ assert_equals(
+ list.getEntries().length,
+ 3,
+ "Observed user timing entries should have only three entries."
+ );
+ });
+}, "Test that invoking observe method twice affects nothing");
+
+promise_test(t => {
+ var promise = new Promise(resolve => {
+ performance.clearMarks();
+ performance.clearMeasures();
+
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe({ entryTypes: ["mark", "measure"] });
+ observer.observe({ entryTypes: ["mark"] });
+ t.add_cleanup(() => observer.disconnect());
+ });
+
+ performance.mark("test-start");
+ performance.mark("test-end");
+ performance.measure("test-measure", "test-start", "test-end");
+
+ return promise.then(list => {
+ assert_equals(
+ list.getEntries().length,
+ 2,
+ "Observed user timing entries should have only two entries."
+ );
+ });
+}, "Test that observing filter is replaced by a new filter");
+
+promise_test(t => {
+ var promise = new Promise(resolve => {
+ performance.clearMarks();
+ performance.clearMeasures();
+
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe({ entryTypes: ["mark"] });
+ observer.observe({ entryTypes: ["measure"] });
+ t.add_cleanup(() => observer.disconnect());
+ });
+
+ performance.mark("test-start");
+ performance.mark("test-end");
+ performance.measure("test-measure", "test-start", "test-end");
+
+ return promise.then(list => {
+ assert_equals(
+ list.getEntries().length,
+ 1,
+ "Observed user timing entries should have only 1 entries."
+ );
+ });
+}, "Test that observing filter is replaced by a new filter");
diff --git a/dom/performance/tests/test_performance_paint_observer.html b/dom/performance/tests/test_performance_paint_observer.html
new file mode 100644
index 0000000000..2ded1db797
--- /dev/null
+++ b/dom/performance/tests/test_performance_paint_observer.html
@@ -0,0 +1,40 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1518999
+ -->
+ <head>
+ <title>Test for Bug 1518999 (Observer API) </title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1518999">Mozilla
+ Bug 1518999 - Paint Timing API For Observers</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ let tab;
+ function runTest() {
+ tab = window.open("test_performance_paint_observer_helper.html");
+ }
+
+ function done() {
+ tab.close();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(runTest);
+ </script>
+ </pre>
+ </div>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_performance_paint_observer_helper.html b/dom/performance/tests/test_performance_paint_observer_helper.html
new file mode 100644
index 0000000000..ae27c9480d
--- /dev/null
+++ b/dom/performance/tests/test_performance_paint_observer_helper.html
@@ -0,0 +1,35 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+ <body>
+ </body>
+ <script>
+ var promise = new Promise(resolve => {
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe({entryTypes: ["paint"]});
+ });
+
+ promise.then(list => {
+ var perfEntries = list.getEntries();
+ opener.is(list.getEntries().length, 1);
+ opener.isDeeply(list.getEntries(),
+ performance.getEntriesByType("paint"),
+ "Observed 'paint' entries should equal to entries obtained by getEntriesByType.");
+ opener.isDeeply(list.getEntries({name: "paint"}),
+ performance.getEntriesByName("paint"),
+ "getEntries with name filter should return correct results.");
+ opener.isDeeply(list.getEntries({entryType: "paint"}),
+ performance.getEntriesByType("paint"),
+ "getEntries with entryType filter should return correct results.");
+ opener.done();
+ });
+
+ const img = document.createElement("IMG");
+ img.src = "http://example.org/tests/dom/performance/tests/logo.png";
+ document.body.appendChild(img);
+
+ </script>
+</html>
diff --git a/dom/performance/tests/test_performance_paint_timing.html b/dom/performance/tests/test_performance_paint_timing.html
new file mode 100644
index 0000000000..f8784ecf26
--- /dev/null
+++ b/dom/performance/tests/test_performance_paint_timing.html
@@ -0,0 +1,38 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1518999
+ -->
+ <head>
+ <title>Test for Bug 1518999</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ </head>
+ <body>
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1518999">Mozilla
+ Bug 1518999 - Paint Timing API</a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ let tab;
+ function runTest() {
+ tab = window.open("test_performance_paint_timing_helper.html");
+ }
+ function done() {
+ tab.close();
+ SimpleTest.finish();
+ }
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(runTest);
+ </script>
+ </pre>
+ </div>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_performance_paint_timing_helper.html b/dom/performance/tests/test_performance_paint_timing_helper.html
new file mode 100644
index 0000000000..c05b38cac0
--- /dev/null
+++ b/dom/performance/tests/test_performance_paint_timing_helper.html
@@ -0,0 +1,65 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1518999
+ -->
+ <head>
+ <title>Test for Bug 1518999</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ </head>
+ <body>
+ <div id="main"></div>
+ <div id="image"></div>
+ <div id="test">
+ <script class="testbody" type="text/javascript">
+ async function runTest() {
+ const paintEntries = performance.getEntriesByType('paint');
+ opener.is(paintEntries.length, 0, "No paint entries yet");
+
+ const img = document.createElement("img");
+ img.src = "http://example.org/tests/dom/performance/tests/logo.png";
+
+ img.onload = function() {
+ function getAndTestEntries(runCount) {
+ function testEntries(entries) {
+ opener.is(entries.length, 1, "FCP Only returns");
+ opener.is(entries[0].entryType, "paint", "entryType is paint");
+ opener.is(entries[0].name, "first-contentful-paint",
+ "Returned entry should be first-contentful-paint" );
+ const fcpEntriesGotByName =
+ performance.getEntriesByName('first-contentful-paint');
+ opener.is(fcpEntriesGotByName.length, 1, "entries length should match");
+ opener.is(entries[0], fcpEntriesGotByName[0], "should be the same entry");
+ opener.done();
+ }
+ const entries = performance.getEntriesByType('paint');
+ if (entries.length < 1) {
+ if (runCount < 4) {
+ opener.SimpleTest.requestFlakyTimeout("FCP is being registered asynchronously, so wait a bit of time");
+ setTimeout(function() {
+ getAndTestEntries(runCount + 1);
+ }, 20);
+ } else {
+ opener.ok(false, "Unable to find paint entries within a reasonable amount of time");
+ opener.done();
+ }
+ } else {
+ testEntries(entries);
+ }
+ }
+ getAndTestEntries(1);
+ }
+ document.body.appendChild(img);
+ }
+ window.onload = function() {
+ runTest();
+ }
+ </script>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_performance_server_timing.html b/dom/performance/tests/test_performance_server_timing.html
new file mode 100644
index 0000000000..cba11a5fdd
--- /dev/null
+++ b/dom/performance/tests/test_performance_server_timing.html
@@ -0,0 +1,58 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Test for PerformanceServerTiming</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+function makeXHR(aUrl) {
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open("get", aUrl, true);
+ xmlhttp.send();
+}
+
+// Note that |responseServerTiming| and |trailerServerTiming| SHOULD be synced with
+// the ones in serverTiming.sjs.
+var responseServerTiming = [{metric:"metric1", duration:"123.4", description:"description1"},
+ {metric:"metric2", duration:"456.78", description:"description2"}];
+var trailerServerTiming = [{metric:"metric3", duration:"789.11", description:"description3"},
+ {metric:"metric4", duration:"1112.13", description:"description4"}];
+
+function checkServerTimingContent(serverTiming) {
+ var expectedResult = responseServerTiming.concat(trailerServerTiming);
+ assert_equals(serverTiming.length, expectedResult.length);
+
+ for (var i = 0; i < expectedResult.length; i++) {
+ assert_equals(serverTiming[i].name, expectedResult[i].metric);
+ assert_equals(serverTiming[i].description, expectedResult[i].description);
+ assert_equals(serverTiming[i].duration, parseFloat(expectedResult[i].duration));
+ }
+}
+
+promise_test(t => {
+ var promise = new Promise(resolve => {
+ performance.clearResourceTimings();
+
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe({entryTypes: ['resource']});
+ t.add_cleanup(() => observer.disconnect());
+ });
+
+ makeXHR("serverTiming.sjs");
+
+ return promise.then(list => {
+ assert_equals(list.getEntries().length, 1);
+ checkServerTimingContent(list.getEntries()[0].serverTiming);
+ });
+}, "server-timing test");
+
+</script>
+</body>
diff --git a/dom/performance/tests/test_performance_server_timing_plain_http.html b/dom/performance/tests/test_performance_server_timing_plain_http.html
new file mode 100644
index 0000000000..7dcb8bd38d
--- /dev/null
+++ b/dom/performance/tests/test_performance_server_timing_plain_http.html
@@ -0,0 +1,42 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Plain HTTP Test for PerformanceServerTiming</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+function makeXHR(aUrl) {
+ var xmlhttp = new XMLHttpRequest();
+ xmlhttp.open("get", aUrl, true);
+ xmlhttp.send();
+}
+
+promise_test(t => {
+ var promise = new Promise(resolve => {
+ performance.clearResourceTimings();
+
+ var observer = new PerformanceObserver(list => resolve(list));
+ observer.observe({entryTypes: ['resource']});
+ t.add_cleanup(() => observer.disconnect());
+ });
+
+ makeXHR("serverTiming.sjs");
+
+ return promise.then(list => {
+ assert_equals(list.getEntries().length, 1);
+ assert_equals(list.getEntries()[0].serverTiming, undefined);
+ assert_equals(list.getEntries()[0].toJSON().serverTiming, undefined,
+ "toJSON should not pick up properties that aren't on the object");
+ });
+}, "server-timing test");
+
+</script>
+</body>
diff --git a/dom/performance/tests/test_performance_timing_json.html b/dom/performance/tests/test_performance_timing_json.html
new file mode 100644
index 0000000000..97079c0d2f
--- /dev/null
+++ b/dom/performance/tests/test_performance_timing_json.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1375829
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1375829</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 1375829 **/
+ var json = performance.timing.toJSON();
+
+ // Ensure it doesn't have any attributes that performance.timing doesn't have
+ for (let key of Object.keys(json)) {
+ ok(key in performance.timing, key + " should be a property of performance.timing");
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1375829">Mozilla Bug 1375829</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/performance/tests/test_performance_user_timing.html b/dom/performance/tests/test_performance_user_timing.html
new file mode 100644
index 0000000000..fa0aaceb4e
--- /dev/null
+++ b/dom/performance/tests/test_performance_user_timing.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=782751
+ -->
+ <head>
+ <title>Test for Bug 782751</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="test_performance_user_timing.js"></script>
+ </head>
+ <body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=782751">Mozilla Bug 782751 - User Timing API</a>
+ <div id="content">
+ </div>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var index = 0;
+
+ function next() {
+ ok(true, "Begin!");
+ var arr;
+ for (var i = 0; i < steps.length; ++i) {
+ try {
+ performance.clearMarks();
+ performance.clearMeasures();
+ performance.clearResourceTimings();
+ is(performance.getEntriesByType("resource").length, 0, "clearing performance resource entries");
+ is(performance.getEntriesByType("mark").length, 0, "clearing performance mark entries");
+ is(performance.getEntriesByType("measure").length, 0, "clearing performance measure entries");
+ steps[i]();
+ } catch(ex) {
+ ok(false, "Caught exception", ex);
+ }
+ }
+
+ SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", reduceTimePrecisionPrevPrefValue);
+ SimpleTest.finish();
+ }
+
+ var reduceTimePrecisionPrevPrefValue = SpecialPowers.getBoolPref("privacy.reduceTimerPrecision");
+ SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", false);
+
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(next);
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_performance_user_timing.js b/dom/performance/tests/test_performance_user_timing.js
new file mode 100644
index 0000000000..c98bee6f11
--- /dev/null
+++ b/dom/performance/tests/test_performance_user_timing.js
@@ -0,0 +1,318 @@
+var steps = [
+ // Test single mark addition
+ function () {
+ ok(true, "Running mark addition test");
+ performance.mark("test");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 1, "Number of marks should be 1");
+ var mark = marks[0];
+ is(mark.name, "test", "mark name should be 'test'");
+ is(mark.entryType, "mark", "mark type should be 'mark'");
+ isnot(mark.startTime, 0, "mark start time should not be 0");
+ is(mark.duration, 0, "mark duration should be 0");
+ },
+ // Test multiple mark addition
+ function () {
+ ok(true, "Running multiple mark with same name addition test");
+ performance.mark("test");
+ performance.mark("test");
+ performance.mark("test");
+ var marks_type = performance.getEntriesByType("mark");
+ is(marks_type.length, 3, "Number of marks by type should be 3");
+ var marks_name = performance.getEntriesByName("test");
+ is(marks_name.length, 3, "Number of marks by name should be 3");
+ var mark = marks_name[0];
+ is(mark.name, "test", "mark name should be 'test'");
+ is(mark.entryType, "mark", "mark type should be 'mark'");
+ isnot(mark.startTime, 0, "mark start time should not be 0");
+ is(mark.duration, 0, "mark duration should be 0");
+ var times = [];
+ // This also tests the chronological ordering specified as
+ // required for getEntries in the performance timeline spec.
+ marks_name.forEach(function (s) {
+ times.forEach(function (time) {
+ ok(
+ s.startTime >= time.startTime,
+ "Times should be equal or increasing between similarly named marks: " +
+ s.startTime +
+ " >= " +
+ time.startTime
+ );
+ });
+ times.push(s);
+ });
+ },
+ // Test all marks removal
+ function () {
+ ok(true, "Running all mark removal test");
+ performance.mark("test");
+ performance.mark("test2");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 2, "number of marks before all removal");
+ performance.clearMarks();
+ marks = performance.getEntriesByType("mark");
+ is(marks.length, 0, "number of marks after all removal");
+ },
+ // Test single mark removal
+ function () {
+ ok(true, "Running removal test (0 'test' marks with other marks)");
+ performance.mark("test2");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 1, "number of marks before all removal");
+ performance.clearMarks("test");
+ marks = performance.getEntriesByType("mark");
+ is(marks.length, 1, "number of marks after all removal");
+ },
+ // Test single mark removal
+ function () {
+ ok(true, "Running removal test (0 'test' marks with no other marks)");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 0, "number of marks before all removal");
+ performance.clearMarks("test");
+ marks = performance.getEntriesByType("mark");
+ is(marks.length, 0, "number of marks after all removal");
+ },
+ function () {
+ ok(true, "Running removal test (1 'test' mark with other marks)");
+ performance.mark("test");
+ performance.mark("test2");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 2, "number of marks before all removal");
+ performance.clearMarks("test");
+ marks = performance.getEntriesByType("mark");
+ is(marks.length, 1, "number of marks after all removal");
+ },
+ function () {
+ ok(true, "Running removal test (1 'test' mark with no other marks)");
+ performance.mark("test");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 1, "number of marks before all removal");
+ performance.clearMarks("test");
+ marks = performance.getEntriesByType("mark");
+ is(marks.length, 0, "number of marks after all removal");
+ },
+ function () {
+ ok(true, "Running removal test (2 'test' marks with other marks)");
+ performance.mark("test");
+ performance.mark("test");
+ performance.mark("test2");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 3, "number of marks before all removal");
+ performance.clearMarks("test");
+ marks = performance.getEntriesByType("mark");
+ is(marks.length, 1, "number of marks after all removal");
+ },
+ function () {
+ ok(true, "Running removal test (2 'test' marks with no other marks)");
+ performance.mark("test");
+ performance.mark("test");
+ var marks = performance.getEntriesByType("mark");
+ is(marks.length, 2, "number of marks before all removal");
+ performance.clearMarks("test");
+ marks = performance.getEntriesByType("mark");
+ is(marks.length, 0, "number of marks after all removal");
+ },
+ // Test mark name being same as navigation timing parameter
+ function () {
+ ok(true, "Running mark name collision test");
+ for (n in performance.timing) {
+ try {
+ if (n == "toJSON") {
+ ok(true, "Skipping toJSON entry in collision test");
+ continue;
+ }
+ performance.mark(n);
+ ok(
+ false,
+ "Mark name collision test failed for name " +
+ n +
+ ", shouldn't make it here!"
+ );
+ } catch (e) {
+ ok(
+ e instanceof DOMException,
+ "DOM exception thrown for mark named " + n
+ );
+ is(
+ e.code,
+ e.SYNTAX_ERR,
+ "DOM exception for name collision is syntax error"
+ );
+ }
+ }
+ },
+ // Test measure
+ function () {
+ ok(true, "Running measure addition with no start/end time test");
+ performance.measure("test");
+ var measures = performance.getEntriesByType("measure");
+ is(measures.length, 1, "number of measures should be 1");
+ var measure = measures[0];
+ is(measure.name, "test", "measure name should be 'test'");
+ is(measure.entryType, "measure", "measure type should be 'measure'");
+ is(measure.startTime, 0, "measure start time should be zero");
+ ok(measure.duration >= 0, "measure duration should not be negative");
+ },
+ function () {
+ ok(true, "Running measure addition with only start time test");
+ performance.mark("test1");
+ performance.measure("test", "test1", undefined);
+ var measures = performance.getEntriesByName("test", "measure");
+ var marks = performance.getEntriesByName("test1", "mark");
+ var measure = measures[0];
+ var mark = marks[0];
+ is(
+ measure.startTime,
+ mark.startTime,
+ "measure start time should be equal to the mark startTime"
+ );
+ ok(measure.duration >= 0, "measure duration should not be negative");
+ },
+ function () {
+ ok(true, "Running measure addition with only end time test");
+ performance.mark("test1");
+ performance.measure("test", undefined, "test1");
+ var measures = performance.getEntriesByName("test", "measure");
+ var marks = performance.getEntriesByName("test1", "mark");
+ var measure = measures[0];
+ var mark = marks[0];
+ ok(measure.duration >= 0, "measure duration should not be negative");
+ },
+ // Test measure picking latest version of similarly named tags
+ function () {
+ ok(true, "Running multiple mark with same name addition test");
+ performance.mark("test");
+ performance.mark("test");
+ performance.mark("test");
+ performance.mark("test2");
+ var marks_name = performance.getEntriesByName("test");
+ is(marks_name.length, 3, "Number of marks by name should be 3");
+ var marks_name2 = performance.getEntriesByName("test2");
+ is(marks_name2.length, 1, "Number of marks by name should be 1");
+ var test_mark = marks_name2[0];
+ performance.measure("test", "test", "test2");
+ var measures_type = performance.getEntriesByType("measure");
+ var last_mark = marks_name[marks_name.length - 1];
+ is(measures_type.length, 1, "Number of measures by type should be 1");
+ var measure = measures_type[0];
+ is(
+ measure.startTime,
+ last_mark.startTime,
+ "Measure start time should be the start time of the latest 'test' mark"
+ );
+ // Tolerance testing to avoid oranges, since we're doing double math across two different languages.
+ ok(
+ measure.duration - (test_mark.startTime - last_mark.startTime) < 0.00001,
+ "Measure duration ( " +
+ measure.duration +
+ ") should be difference between two marks"
+ );
+ },
+ function () {
+ // We don't have navigationStart in workers.
+ if ("window" in self) {
+ ok(true, "Running measure addition with no start/end time test");
+ performance.measure("test", "navigationStart");
+ var measures = performance.getEntriesByType("measure");
+ is(measures.length, 1, "number of measures should be 1");
+ var measure = measures[0];
+ is(measure.name, "test", "measure name should be 'test'");
+ is(measure.entryType, "measure", "measure type should be 'measure'");
+ is(measure.startTime, 0, "measure start time should be zero");
+ ok(measure.duration >= 0, "measure duration should not be negative");
+ }
+ },
+ // Test all measure removal
+ function () {
+ ok(true, "Running all measure removal test");
+ performance.measure("test");
+ performance.measure("test2");
+ var measures = performance.getEntriesByType("measure");
+ is(measures.length, 2, "measure entries should be length 2");
+ performance.clearMeasures();
+ measures = performance.getEntriesByType("measure");
+ is(measures.length, 0, "measure entries should be length 0");
+ },
+ // Test single measure removal
+ function () {
+ ok(true, "Running all measure removal test");
+ performance.measure("test");
+ performance.measure("test2");
+ var measures = performance.getEntriesByType("measure");
+ is(measures.length, 2, "measure entries should be length 2");
+ performance.clearMeasures("test");
+ measures = performance.getEntriesByType("measure");
+ is(measures.length, 1, "measure entries should be length 1");
+ },
+ // Test measure with invalid start time mark name
+ function () {
+ ok(true, "Running measure invalid start test");
+ try {
+ performance.measure("test", "notamark");
+ ok(false, "invalid measure start time exception not thrown!");
+ } catch (e) {
+ ok(e instanceof DOMException, "DOM exception thrown for invalid measure");
+ is(
+ e.code,
+ e.SYNTAX_ERR,
+ "DOM exception for invalid time is syntax error"
+ );
+ }
+ },
+ // Test measure with invalid end time mark name
+ function () {
+ ok(true, "Running measure invalid end test");
+ try {
+ performance.measure("test", undefined, "notamark");
+ ok(false, "invalid measure end time exception not thrown!");
+ } catch (e) {
+ ok(e instanceof DOMException, "DOM exception thrown for invalid measure");
+ is(
+ e.code,
+ e.SYNTAX_ERR,
+ "DOM exception for invalid time is syntax error"
+ );
+ }
+ },
+ // Test measure name being same as navigation timing parameter
+ function () {
+ ok(true, "Running measure name collision test");
+ for (n in performance.timing) {
+ if (n == "toJSON") {
+ ok(true, "Skipping toJSON entry in collision test");
+ continue;
+ }
+ performance.measure(n);
+ ok(true, "Measure name supports name collisions: " + n);
+ }
+ },
+ // Test measure mark being a reserved name
+ function () {
+ ok(true, "Create measures using all reserved names");
+ for (n in performance.timing) {
+ try {
+ if (n == "toJSON") {
+ ok(true, "Skipping toJSON entry in collision test");
+ continue;
+ }
+ performance.measure("test", n);
+ ok(true, "Measure created from reserved name as starting time: " + n);
+ } catch (e) {
+ ok(
+ [
+ "redirectStart",
+ "redirectEnd",
+ "unloadEventStart",
+ "unloadEventEnd",
+ "loadEventEnd",
+ "secureConnectionStart",
+ ].includes(n),
+ "Measure created from reserved name as starting time: " +
+ n +
+ " and threw expected error"
+ );
+ }
+ }
+ },
+ // TODO: Test measure picking latest version of similarly named tags
+];
diff --git a/dom/performance/tests/test_performance_user_timing_dying_global.html b/dom/performance/tests/test_performance_user_timing_dying_global.html
new file mode 100644
index 0000000000..18e4a54684
--- /dev/null
+++ b/dom/performance/tests/test_performance_user_timing_dying_global.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Test for User Timing APIs on dying globals</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript">
+ // We must wait for the iframe to load.
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener('load', () => {
+ const dyingWindow = initTest();
+ ok(true, 'Initialization complete');
+
+ testDoesNotCrash(dyingWindow);
+ SimpleTest.finish();
+ });
+
+ function initTest() {
+ // We create a dying global by creating an iframe, keeping a
+ // reference to it, and removing it.
+ const iframe = document.querySelector('iframe');
+ const iframeWindow = iframe.contentWindow;
+
+ // We want to call the User Timing functions in the context of
+ // the dying global. However, we can't call constructors
+ // directly on a reference to a window so we have to wrap it.
+ iframeWindow.newPerformanceMark = () => {
+ new PerformanceMark('constructor', {detail: 'constructorDetail'});
+ };
+
+ // Send the global to a dying state.
+ iframe.remove();
+
+ return iframeWindow;
+ }
+
+ function testDoesNotCrash(dyingWindow) {
+ ok(true, 'Running testDoesNotCrash');
+
+ dyingWindow.newPerformanceMark();
+ ok(true, 'new PerformanceMark() on dying global did not crash');
+
+ try {
+ dyingWindow.performance.mark('markMethod', {detail: 'markMethodDetail'});
+ } catch (e) {
+ is(e.code, e.INVALID_STATE_ERR, 'performance.mark on dying global threw expected exception');
+ }
+ ok(true, 'performance.mark on dying global did not crash');
+
+ try {
+ dyingWindow.performance.measure('measureMethod');
+ } catch (e) {
+ is(e.code, e.INVALID_STATE_ERR, 'performance.measure on dying global threw expected exception');
+ }
+ ok(true, 'performance.measure on dying global did not crash');
+ }
+ </script>
+ </head>
+ <body>
+ <iframe width="200" height="200" src="about:blank"></iframe>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_sharedWorker_performance_user_timing.html b/dom/performance/tests/test_sharedWorker_performance_user_timing.html
new file mode 100644
index 0000000000..d26594e292
--- /dev/null
+++ b/dom/performance/tests/test_sharedWorker_performance_user_timing.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for worker performance timing API</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <script class="testbody" type="text/javascript">
+
+var sw = new SharedWorker('sharedworker_performance_user_timing.js');
+sw.port.onmessage = function(event) {
+ if (event.data.type == 'finish') {
+ SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", reduceTimePrecisionPrevPrefValue);
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ }
+}
+
+var reduceTimePrecisionPrevPrefValue = SpecialPowers.getBoolPref("privacy.reduceTimerPrecision");
+SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", false);
+SimpleTest.waitForExplicitFinish();
+ </script>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_timeOrigin.html b/dom/performance/tests/test_timeOrigin.html
new file mode 100644
index 0000000000..69796a432d
--- /dev/null
+++ b/dom/performance/tests/test_timeOrigin.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for performance.timeOrigin</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <script type="text/js-worker" id="worker-src">
+ postMessage({ now: performance.now(), timeOrigin: performance.timeOrigin });
+ </script>
+
+ <script type="text/js-worker" id="shared-worker-src">
+ onconnect = function(evt) {
+ evt.ports[0].postMessage({ now: performance.now(), timeOrigin: performance.timeOrigin });
+ };
+ </script>
+
+ <script class="testbody" type="text/javascript">
+
+function testBasic() {
+ ok("timeOrigin" in performance, "Performance.timeOrigin exists.");
+ ok(performance.timeOrigin > 0, "TimeOrigin must be greater than 0.");
+ next();
+}
+
+function testWorker() {
+ var now = performance.now();
+
+ var blob = new Blob([ document.getElementById("worker-src").textContent ],
+ { type: "text/javascript" });
+ var w = new Worker(URL.createObjectURL(blob));
+ w.onmessage = function(e) {
+ ok (e.data.now + e.data.timeOrigin > now + performance.timeOrigin, "Comparing worker.now and window.now");
+ next();
+ }
+}
+
+function testSharedWorker() {
+ var now = performance.now();
+
+ var blob = new Blob([ document.getElementById("shared-worker-src").textContent ],
+ { type: "text/javascript" });
+ var w = new SharedWorker(URL.createObjectURL(blob));
+ w.port.onmessage = function(e) {
+ ok (e.data.now + e.data.timeOrigin > now + performance.timeOrigin, "Comparing worker.now and window.now");
+ next();
+ }
+}
+
+var tests = [ testBasic, testWorker, testSharedWorker ];
+function next() {
+ if (!tests.length) {
+ SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", reduceTimePrecisionPrevPrefValue);
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+// It is a known issue that comparing time between a worker and a window
+// when timer clamping is in effect may cause time to go backwards.
+// Do not run this test with this preference set. For large values of
+// clamping you will see failures. For small values, it is intermitant.
+var reduceTimePrecisionPrevPrefValue = SpecialPowers.getBoolPref("privacy.reduceTimerPrecision");
+SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", false);
+
+addLoadEvent(next);
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/performance/tests/test_worker_observer.html b/dom/performance/tests/test_worker_observer.html
new file mode 100644
index 0000000000..7f4df855c9
--- /dev/null
+++ b/dom/performance/tests/test_worker_observer.html
@@ -0,0 +1,41 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset=utf-8>
+<title>Test for performance observer in worker</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<div id="log"></div>
+<script>
+const worker = new Worker("worker_performance_observer.js");
+
+promise_test(t => {
+ let found = false;
+ return new Promise(resolve => {
+ SpecialPowers.registerConsoleListener(msg => {
+ if (msg.errorMessage === "Ignoring unsupported entryTypes: invalid.") {
+ found = true;
+ resolve();
+ }
+ });
+ worker.addEventListener("error", resolve);
+ worker.addEventListener("message", function(event) {
+ if (event.data.type === "complete") {
+ resolve();
+ }
+ });
+ }).then(() => {
+ SpecialPowers.postConsoleSentinel();
+ assert_true(found, "got the expected console warning");
+ });
+}, "Console warnings about invalid types should be logged during the tests");
+
+fetch_tests_from_worker(worker);
+</script>
+</body>
diff --git a/dom/performance/tests/test_worker_performance_entries.html b/dom/performance/tests/test_worker_performance_entries.html
new file mode 100644
index 0000000000..d3f124fdb3
--- /dev/null
+++ b/dom/performance/tests/test_worker_performance_entries.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PerformanceResouceTiming in workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// The worker assumes it will take some amount of time to load a resource.
+// With a low enough precision, the duration to load a resource may clamp
+// down to zero.
+var reduceTimePrecisionPrevPrefValue = SpecialPowers.getBoolPref("privacy.reduceTimerPrecision");
+SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", false);
+
+var worker = new Worker('test_worker_performance_entries.js');
+worker.onmessage = function(event) {
+ if (event.data.type == "check") {
+ ok(event.data.status, event.data.msg);
+ return;
+ }
+
+ if (event.data.type == "finish") {
+ SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", reduceTimePrecisionPrevPrefValue);
+ SimpleTest.finish();
+ return;
+ }
+
+ ok(false, "?!?");
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/performance/tests/test_worker_performance_entries.js b/dom/performance/tests/test_worker_performance_entries.js
new file mode 100644
index 0000000000..e3d660bd85
--- /dev/null
+++ b/dom/performance/tests/test_worker_performance_entries.js
@@ -0,0 +1,120 @@
+function ok(a, msg) {
+ postMessage({ type: "check", status: !!a, msg });
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+function finish(a, msg) {
+ postMessage({ type: "finish" });
+}
+
+async function wait_for_performance_entries() {
+ let promise = new Promise(resolve => {
+ new PerformanceObserver(list => {
+ resolve(list.getEntries());
+ }).observe({ entryTypes: ["resource"] });
+ });
+ entries = await promise;
+ return entries;
+}
+
+async function check(resource, initiatorType, protocol) {
+ let entries = performance.getEntries();
+ if (!entries.length) {
+ entries = await wait_for_performance_entries();
+ }
+ ok(entries.length == 1, "We have an entry");
+
+ ok(entries[0] instanceof PerformanceEntry, "The entry is a PerformanceEntry");
+ ok(entries[0].name.endsWith(resource), "The entry has been found!");
+
+ is(entries[0].entryType, "resource", "Correct EntryType");
+ ok(entries[0].startTime > 0, "We have a startTime");
+ ok(entries[0].duration > 0, "We have a duration");
+
+ ok(
+ entries[0] instanceof PerformanceResourceTiming,
+ "The entry is a PerformanceResourceTiming"
+ );
+
+ is(entries[0].initiatorType, initiatorType, "Correct initiatorType");
+ is(entries[0].nextHopProtocol, protocol, "Correct protocol");
+
+ performance.clearResourceTimings();
+}
+
+function simple_checks() {
+ ok("performance" in self, "We have self.performance");
+ performance.clearResourceTimings();
+ next();
+}
+
+function fetch_request() {
+ fetch("test_worker_performance_entries.sjs")
+ .then(r => r.blob())
+ .then(blob => {
+ check("test_worker_performance_entries.sjs", "fetch", "http/1.1");
+ })
+ .then(next);
+}
+
+function xhr_request() {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "test_worker_performance_entries.sjs");
+ xhr.send();
+ xhr.onload = () => {
+ check("test_worker_performance_entries.sjs", "xmlhttprequest", "http/1.1");
+ next();
+ };
+}
+
+function sync_xhr_request() {
+ let xhr = new XMLHttpRequest();
+ xhr.open("GET", "test_worker_performance_entries.sjs", false);
+ xhr.send();
+ check("test_worker_performance_entries.sjs", "xmlhttprequest", "http/1.1");
+ next();
+}
+
+function import_script() {
+ importScripts(["empty.js"]);
+ check("empty.js", "other", "http/1.1");
+ next();
+}
+
+function redirect() {
+ fetch("test_worker_performance_entries.sjs?redirect")
+ .then(r => r.text())
+ .then(async text => {
+ is(text, "Hello world \\o/", "The redirect worked correctly");
+ await check(
+ "test_worker_performance_entries.sjs?redirect",
+ "fetch",
+ "http/1.1"
+ );
+ })
+ .then(next);
+}
+
+let tests = [
+ simple_checks,
+ fetch_request,
+ xhr_request,
+ sync_xhr_request,
+ import_script,
+ redirect,
+];
+
+function next() {
+ if (!tests.length) {
+ finish();
+ return;
+ }
+
+ let test = tests.shift();
+ test();
+}
+
+next();
diff --git a/dom/performance/tests/test_worker_performance_entries.sjs b/dom/performance/tests/test_worker_performance_entries.sjs
new file mode 100644
index 0000000000..62f00c22bc
--- /dev/null
+++ b/dom/performance/tests/test_worker_performance_entries.sjs
@@ -0,0 +1,11 @@
+function handleRequest(request, response) {
+ response.setHeader("Content-Type", "text/html");
+
+ if (request.queryString == "redirect") {
+ response.setStatusLine(request.httpVersion, 302, "See Other");
+ response.setHeader("Location", "test_worker_performance_entries.sjs?ok");
+ } else {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write("Hello world \\o/");
+ }
+}
diff --git a/dom/performance/tests/test_worker_performance_now.html b/dom/performance/tests/test_worker_performance_now.html
new file mode 100644
index 0000000000..be4f8f56ea
--- /dev/null
+++ b/dom/performance/tests/test_worker_performance_now.html
@@ -0,0 +1,31 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Validate Interfaces Exposed to Workers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var reduceTimePrecisionPrevPrefValue = SpecialPowers.getBoolPref("privacy.reduceTimerPrecision");
+SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", false);
+
+var worker = new Worker('test_worker_performance_now.js');
+worker.onmessage = function(event) {
+ if (event.data.type == 'finish') {
+ SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", reduceTimePrecisionPrevPrefValue);
+ SimpleTest.finish();
+
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/performance/tests/test_worker_performance_now.js b/dom/performance/tests/test_worker_performance_now.js
new file mode 100644
index 0000000000..a22f66256e
--- /dev/null
+++ b/dom/performance/tests/test_worker_performance_now.js
@@ -0,0 +1,68 @@
+function ok(a, msg) {
+ dump("OK: " + !!a + " => " + a + ": " + msg + "\n");
+ postMessage({ type: "status", status: !!a, msg: a + ": " + msg });
+}
+
+function workerTestDone() {
+ postMessage({ type: "finish" });
+}
+
+ok(self.performance, "Performance object should exist.");
+ok(
+ typeof self.performance.now == "function",
+ "Performance object should have a 'now' method."
+);
+var n = self.performance.now(),
+ d = Date.now();
+ok(n >= 0, "The value of now() should be equal to or greater than 0.");
+ok(
+ self.performance.now() >= n,
+ "The value of now() should monotonically increase."
+);
+
+// Spin on setTimeout() until performance.now() increases. Due to recent
+// security developments, the hr-time working group has not yet reached
+// consensus on what the recommend minimum clock resolution should be:
+// https://w3c.github.io/hr-time/#clock-resolution
+// Since setTimeout might return too early/late, our goal is for
+// performance.now() to increase before a 2 ms deadline rather than specific
+// number of setTimeout(N) invocations.
+// See bug 749894 (intermittent failures of this test)
+setTimeout(checkAfterTimeout, 1);
+
+var checks = 0;
+
+function checkAfterTimeout() {
+ checks++;
+ var d2 = Date.now();
+ var n2 = self.performance.now();
+
+ // Spin on setTimeout() until performance.now() increases. Abort the test
+ // if it runs for more than 2 ms or 50 timeouts.
+ let elapsedTime = d2 - d;
+ let elapsedPerf = n2 - n;
+ if (elapsedPerf == 0 && elapsedTime < 2 && checks < 50) {
+ setTimeout(checkAfterTimeout, 1);
+ return;
+ }
+
+ // Our implementation provides 1 ms resolution (bug 1451790), but we
+ // can't assert that elapsedPerf >= 1 ms because this worker test runs with
+ // "privacy.reduceTimerPrecision" == false so performance.now() is not
+ // limited to 1 ms resolution.
+ ok(
+ elapsedPerf > 0,
+ `Loose - the value of now() should increase after 2ms. ` +
+ `delta now(): ${elapsedPerf} ms`
+ );
+
+ // If we need more than 1 iteration, then either performance.now() resolution
+ // is shorter than 1 ms or setTimeout() is returning too early.
+ ok(
+ checks == 1,
+ `Strict - the value of now() should increase after one setTimeout. ` +
+ `iters: ${checks}, dt: ${elapsedTime}, now(): ${n2}`
+ );
+
+ workerTestDone();
+}
diff --git a/dom/performance/tests/test_worker_user_timing.html b/dom/performance/tests/test_worker_user_timing.html
new file mode 100644
index 0000000000..ebeac24e4f
--- /dev/null
+++ b/dom/performance/tests/test_worker_user_timing.html
@@ -0,0 +1,30 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test for worker performance timing API</title>
+ <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ </head>
+ <body>
+ <script class="testbody" type="text/javascript">
+
+var worker = new Worker('worker_performance_user_timing.js');
+worker.onmessage = function(event) {
+ if (event.data.type == 'finish') {
+ SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", reduceTimePrecisionPrevPrefValue);
+ SimpleTest.finish();
+ } else if (event.data.type == 'status') {
+ ok(event.data.status, event.data.msg);
+ }
+}
+
+var reduceTimePrecisionPrevPrefValue = SpecialPowers.getBoolPref("privacy.reduceTimerPrecision");
+SpecialPowers.setBoolPref("privacy.reduceTimerPrecision", false);
+SimpleTest.waitForExplicitFinish();
+ </script>
+ </body>
+</html>
diff --git a/dom/performance/tests/worker_performance_observer.js b/dom/performance/tests/worker_performance_observer.js
new file mode 100644
index 0000000000..3282c9d157
--- /dev/null
+++ b/dom/performance/tests/worker_performance_observer.js
@@ -0,0 +1,4 @@
+importScripts(["/resources/testharness.js"]);
+importScripts(["test_performance_observer.js"]);
+
+done();
diff --git a/dom/performance/tests/worker_performance_user_timing.js b/dom/performance/tests/worker_performance_user_timing.js
new file mode 100644
index 0000000000..257040f09f
--- /dev/null
+++ b/dom/performance/tests/worker_performance_user_timing.js
@@ -0,0 +1,32 @@
+function ok(a, msg) {
+ dump("OK: " + !!a + " => " + a + " " + msg + "\n");
+ postMessage({ type: "status", status: !!a, msg: a + ": " + msg });
+}
+
+function is(a, b, msg) {
+ dump("IS: " + (a === b) + " => " + a + " | " + b + " " + msg + "\n");
+ postMessage({
+ type: "status",
+ status: a === b,
+ msg: a + " === " + b + ": " + msg,
+ });
+}
+
+function isnot(a, b, msg) {
+ dump("ISNOT: " + (a === b) + " => " + a + " | " + b + " " + msg + "\n");
+ postMessage({
+ type: "status",
+ status: a != b,
+ msg: a + " != " + b + ": " + msg,
+ });
+}
+
+importScripts(["test_performance_user_timing.js"]);
+
+for (var i = 0; i < steps.length; ++i) {
+ performance.clearMarks();
+ performance.clearMeasures();
+ steps[i]();
+}
+
+postMessage({ type: "finish" });