diff options
Diffstat (limited to '')
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 Binary files differnew file mode 100644 index 0000000000..a05926bcd7 --- /dev/null +++ b/dom/performance/tests/logo.png 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" }); |