From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/performance/EventCounts.cpp | 79 ++ dom/performance/EventCounts.h | 31 + dom/performance/LargestContentfulPaint.cpp | 85 ++ dom/performance/LargestContentfulPaint.h | 68 ++ dom/performance/Performance.cpp | 1051 ++++++++++++++++++++ dom/performance/Performance.h | 253 +++++ dom/performance/PerformanceEntry.cpp | 54 + dom/performance/PerformanceEntry.h | 104 ++ dom/performance/PerformanceEventTiming.cpp | 206 ++++ dom/performance/PerformanceEventTiming.h | 136 +++ dom/performance/PerformanceMainThread.cpp | 565 +++++++++++ dom/performance/PerformanceMainThread.h | 141 +++ dom/performance/PerformanceMark.cpp | 124 +++ dom/performance/PerformanceMark.h | 68 ++ dom/performance/PerformanceMeasure.cpp | 62 ++ dom/performance/PerformanceMeasure.h | 49 + dom/performance/PerformanceNavigation.cpp | 31 + dom/performance/PerformanceNavigation.h | 50 + dom/performance/PerformanceNavigationTiming.cpp | 156 +++ dom/performance/PerformanceNavigationTiming.h | 93 ++ dom/performance/PerformanceObserver.cpp | 367 +++++++ dom/performance/PerformanceObserver.h | 91 ++ dom/performance/PerformanceObserverEntryList.cpp | 100 ++ dom/performance/PerformanceObserverEntryList.h | 55 + dom/performance/PerformancePaintTiming.cpp | 52 + dom/performance/PerformancePaintTiming.h | 50 + dom/performance/PerformanceResourceTiming.cpp | 134 +++ dom/performance/PerformanceResourceTiming.h | 172 ++++ dom/performance/PerformanceServerTiming.cpp | 71 ++ dom/performance/PerformanceServerTiming.h | 52 + dom/performance/PerformanceService.cpp | 42 + dom/performance/PerformanceService.h | 43 + dom/performance/PerformanceStorage.h | 35 + dom/performance/PerformanceStorageWorker.cpp | 184 ++++ dom/performance/PerformanceStorageWorker.h | 48 + dom/performance/PerformanceTiming.cpp | 676 +++++++++++++ dom/performance/PerformanceTiming.h | 595 +++++++++++ dom/performance/PerformanceTimingTypes.ipdlh | 50 + dom/performance/PerformanceWorker.cpp | 64 ++ dom/performance/PerformanceWorker.h | 100 ++ dom/performance/moz.build | 65 ++ dom/performance/tests/empty.js | 1 + dom/performance/tests/logo.png | Bin 0 -> 21901 bytes dom/performance/tests/mochitest.ini | 44 + dom/performance/tests/serverTiming.sjs | 41 + .../tests/sharedworker_performance_user_timing.js | 38 + .../tests/test_performance_navigation_timing.html | 104 ++ .../tests/test_performance_observer.html | 142 +++ dom/performance/tests/test_performance_observer.js | 286 ++++++ .../tests/test_performance_paint_observer.html | 40 + .../test_performance_paint_observer_helper.html | 35 + .../tests/test_performance_paint_timing.html | 38 + .../test_performance_paint_timing_helper.html | 65 ++ .../tests/test_performance_server_timing.html | 58 ++ .../test_performance_server_timing_plain_http.html | 42 + .../tests/test_performance_timing_json.html | 32 + .../tests/test_performance_user_timing.html | 49 + .../tests/test_performance_user_timing.js | 318 ++++++ .../test_performance_user_timing_dying_global.html | 61 ++ .../test_sharedWorker_performance_user_timing.html | 30 + dom/performance/tests/test_timeOrigin.html | 76 ++ dom/performance/tests/test_worker_observer.html | 41 + .../tests/test_worker_performance_entries.html | 39 + .../tests/test_worker_performance_entries.js | 120 +++ .../tests/test_worker_performance_entries.sjs | 11 + .../tests/test_worker_performance_now.html | 31 + .../tests/test_worker_performance_now.js | 68 ++ dom/performance/tests/test_worker_user_timing.html | 30 + .../tests/worker_performance_observer.js | 4 + .../tests/worker_performance_user_timing.js | 32 + 70 files changed, 8328 insertions(+) create mode 100644 dom/performance/EventCounts.cpp create mode 100644 dom/performance/EventCounts.h create mode 100644 dom/performance/LargestContentfulPaint.cpp create mode 100644 dom/performance/LargestContentfulPaint.h create mode 100644 dom/performance/Performance.cpp create mode 100644 dom/performance/Performance.h create mode 100644 dom/performance/PerformanceEntry.cpp create mode 100644 dom/performance/PerformanceEntry.h create mode 100644 dom/performance/PerformanceEventTiming.cpp create mode 100644 dom/performance/PerformanceEventTiming.h create mode 100644 dom/performance/PerformanceMainThread.cpp create mode 100644 dom/performance/PerformanceMainThread.h create mode 100644 dom/performance/PerformanceMark.cpp create mode 100644 dom/performance/PerformanceMark.h create mode 100644 dom/performance/PerformanceMeasure.cpp create mode 100644 dom/performance/PerformanceMeasure.h create mode 100644 dom/performance/PerformanceNavigation.cpp create mode 100644 dom/performance/PerformanceNavigation.h create mode 100644 dom/performance/PerformanceNavigationTiming.cpp create mode 100644 dom/performance/PerformanceNavigationTiming.h create mode 100644 dom/performance/PerformanceObserver.cpp create mode 100644 dom/performance/PerformanceObserver.h create mode 100644 dom/performance/PerformanceObserverEntryList.cpp create mode 100644 dom/performance/PerformanceObserverEntryList.h create mode 100644 dom/performance/PerformancePaintTiming.cpp create mode 100644 dom/performance/PerformancePaintTiming.h create mode 100644 dom/performance/PerformanceResourceTiming.cpp create mode 100644 dom/performance/PerformanceResourceTiming.h create mode 100644 dom/performance/PerformanceServerTiming.cpp create mode 100644 dom/performance/PerformanceServerTiming.h create mode 100644 dom/performance/PerformanceService.cpp create mode 100644 dom/performance/PerformanceService.h create mode 100644 dom/performance/PerformanceStorage.h create mode 100644 dom/performance/PerformanceStorageWorker.cpp create mode 100644 dom/performance/PerformanceStorageWorker.h create mode 100644 dom/performance/PerformanceTiming.cpp create mode 100644 dom/performance/PerformanceTiming.h create mode 100644 dom/performance/PerformanceTimingTypes.ipdlh create mode 100644 dom/performance/PerformanceWorker.cpp create mode 100644 dom/performance/PerformanceWorker.h create mode 100644 dom/performance/moz.build create mode 100644 dom/performance/tests/empty.js create mode 100644 dom/performance/tests/logo.png create mode 100644 dom/performance/tests/mochitest.ini create mode 100644 dom/performance/tests/serverTiming.sjs create mode 100644 dom/performance/tests/sharedworker_performance_user_timing.js create mode 100644 dom/performance/tests/test_performance_navigation_timing.html create mode 100644 dom/performance/tests/test_performance_observer.html create mode 100644 dom/performance/tests/test_performance_observer.js create mode 100644 dom/performance/tests/test_performance_paint_observer.html create mode 100644 dom/performance/tests/test_performance_paint_observer_helper.html create mode 100644 dom/performance/tests/test_performance_paint_timing.html create mode 100644 dom/performance/tests/test_performance_paint_timing_helper.html create mode 100644 dom/performance/tests/test_performance_server_timing.html create mode 100644 dom/performance/tests/test_performance_server_timing_plain_http.html create mode 100644 dom/performance/tests/test_performance_timing_json.html create mode 100644 dom/performance/tests/test_performance_user_timing.html create mode 100644 dom/performance/tests/test_performance_user_timing.js create mode 100644 dom/performance/tests/test_performance_user_timing_dying_global.html create mode 100644 dom/performance/tests/test_sharedWorker_performance_user_timing.html create mode 100644 dom/performance/tests/test_timeOrigin.html create mode 100644 dom/performance/tests/test_worker_observer.html create mode 100644 dom/performance/tests/test_worker_performance_entries.html create mode 100644 dom/performance/tests/test_worker_performance_entries.js create mode 100644 dom/performance/tests/test_worker_performance_entries.sjs create mode 100644 dom/performance/tests/test_worker_performance_now.html create mode 100644 dom/performance/tests/test_worker_performance_now.js create mode 100644 dom/performance/tests/test_worker_user_timing.html create mode 100644 dom/performance/tests/worker_performance_observer.js create mode 100644 dom/performance/tests/worker_performance_user_timing.js (limited to 'dom/performance') 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 global = do_QueryInterface(GetParentObject()); + if (global) { + MOZ_ASSERT(global->IsDying()); + } +#endif + return; + } + } +} + +JSObject* EventCounts::WrapObject(JSContext* aCx, + JS::Handle 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 aGivenProto) override; + + private: + ~EventCounts() = default; + nsCOMPtr mParent; +}; +} // namespace mozilla::dom +#endif diff --git a/dom/performance/LargestContentfulPaint.cpp b/dom/performance/LargestContentfulPaint.cpp new file mode 100644 index 0000000000..29984ade73 --- /dev/null +++ b/dom/performance/LargestContentfulPaint.cpp @@ -0,0 +1,85 @@ +/* -*- 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 "nsRFPService.h" +#include "Performance.h" +#include "PerformanceMainThread.h" +#include "LargestContentfulPaint.h" + +namespace mozilla::dom { + +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) + +LargestContentfulPaint::LargestContentfulPaint(Performance* aPerformance, + DOMHighResTimeStamp aRenderTime, + DOMHighResTimeStamp aLoadTime, + unsigned long aSize, + nsIURI* aURI, Element* aElement) + : PerformanceEntry(aPerformance->GetParentObject(), u""_ns, + kLargestContentfulPaintName), + mPerformance(static_cast(aPerformance)), + mRenderTime(aRenderTime), + mLoadTime(aLoadTime), + mSize(aSize), + mURI(aURI), + mElement(aElement) { + MOZ_ASSERT(mPerformance); + MOZ_ASSERT(mElement); + // The element could be a pseudo-element + if (mElement->ChromeOnlyAccess()) { + mElement = Element::FromNodeOrNull( + aElement->FindFirstNonChromeOnlyAccessContent()); + } + + if (const Element* element = GetElement()) { + mId = element->GetID(); + } +} + +JSObject* LargestContentfulPaint::WrapObject( + JSContext* aCx, JS::Handle aGivenProto) { + return LargestContentfulPaint_Binding::Wrap(aCx, this, aGivenProto); +} + +Element* LargestContentfulPaint::GetElement() const { + return mElement ? nsContentUtils::GetAnElementForTiming( + mElement, mElement->GetComposedDoc(), nullptr) + : nullptr; +} + +DOMHighResTimeStamp LargestContentfulPaint::RenderTime() const { + return nsRFPService::ReduceTimePrecisionAsMSecs( + mRenderTime, mPerformance->GetRandomTimelineSeed(), + mPerformance->GetRTPCallerType()); +} + +DOMHighResTimeStamp LargestContentfulPaint::LoadTime() const { + return nsRFPService::ReduceTimePrecisionAsMSecs( + mLoadTime, mPerformance->GetRandomTimelineSeed(), + mPerformance->GetRTPCallerType()); +} + +DOMHighResTimeStamp LargestContentfulPaint::StartTime() const { + DOMHighResTimeStamp startTime = !mRenderTime ? mLoadTime : mRenderTime; + return nsRFPService::ReduceTimePrecisionAsMSecs( + startTime, mPerformance->GetRandomTimelineSeed(), + mPerformance->GetRTPCallerType()); +} + +void LargestContentfulPaint::GetUrl(nsAString& aUrl) { + if (mURI) { + CopyUTF8toUTF16(mURI->GetSpecOrDefault(), aUrl); + } +} +} // namespace mozilla::dom diff --git a/dom/performance/LargestContentfulPaint.h b/dom/performance/LargestContentfulPaint.h new file mode 100644 index 0000000000..e32eb37970 --- /dev/null +++ b/dom/performance/LargestContentfulPaint.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_LargestContentfulPaint_h___ +#define mozilla_dom_LargestContentfulPaint_h___ + +#include "nsCycleCollectionParticipant.h" +#include "mozilla/dom/PerformanceEntry.h" +#include "mozilla/dom/PerformanceLargestContentfulPaintBinding.h" +#include "nsIWeakReferenceUtils.h" + +class nsTextFrame; +namespace mozilla::dom { + +static constexpr nsLiteralString kLargestContentfulPaintName = + u"largest-contentful-paint"_ns; + +class Performance; +class PerformanceMainThread; + +// 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(Performance* aPerformance, + DOMHighResTimeStamp aRenderTime, + DOMHighResTimeStamp aLoadTime, unsigned long aSize, + nsIURI* aURI, Element* aElement); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle 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; + + private: + ~LargestContentfulPaint() = default; + + RefPtr mPerformance; + + DOMHighResTimeStamp mRenderTime; + DOMHighResTimeStamp mLoadTime; + unsigned long mSize; + nsCOMPtr mURI; + + RefPtr mElement; + RefPtr mId; +}; +} // namespace mozilla::dom +#endif diff --git a/dom/performance/Performance.cpp b/dom/performance/Performance.cpp new file mode 100644 index 0000000000..a4e161a730 --- /dev/null +++ b/dom/performance/Performance.cpp @@ -0,0 +1,1051 @@ +/* -*- 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 + +#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::CreateForMainThread( + nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel) { + MOZ_ASSERT(NS_IsMainThread()); + + MOZ_ASSERT(aWindow->AsGlobal()); + RefPtr performance = + new PerformanceMainThread(aWindow, aDOMTiming, aChannel); + return performance.forget(); +} + +/* static */ +already_AddRefed Performance::CreateForWorker( + WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr performance = new PerformanceWorker(aWorkerPrivate); + return performance.forget(); +} + +/* static */ +already_AddRefed Performance::Get(JSContext* aCx, + nsIGlobalObject* aGlobal) { + RefPtr performance; + nsCOMPtr window = do_QueryInterface(aGlobal); + if (window) { + performance = window->GetPerformance(); + } else { + 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::Unknown)) {} + +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 aGivenProto) { + return Performance_Binding::Wrap(aCx, this, aGivenProto); +} + +void Performance::GetEntries(nsTArray>& aRetval) { + aRetval = mResourceEntries.Clone(); + aRetval.AppendElements(mUserEntries); + aRetval.Sort(PerformanceEntryComparator()); +} + +void Performance::GetEntriesByType( + const nsAString& aEntryType, nsTArray>& aRetval) { + if (aEntryType.EqualsLiteral("resource")) { + aRetval = mResourceEntries.Clone(); + return; + } + + aRetval.Clear(); + + if (aEntryType.EqualsLiteral("mark") || aEntryType.EqualsLiteral("measure")) { + RefPtr entryType = NS_Atomize(aEntryType); + for (PerformanceEntry* entry : mUserEntries) { + if (entry->GetEntryType() == entryType) { + aRetval.AppendElement(entry); + } + } + } +} + +void Performance::GetEntriesByName( + const nsAString& aName, const Optional& aEntryType, + nsTArray>& aRetval) { + aRetval.Clear(); + + RefPtr name = NS_Atomize(aName); + RefPtr 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 qualifiedResourceEntries; + nsTArray 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>& aRetval) { + GetEntriesByType(aEntryType, aRetval); +} + +void Performance::ClearUserEntries(const Optional& aEntryName, + const nsAString& aEntryType) { + MOZ_ASSERT(!aEntryType.IsEmpty()); + RefPtr name = + aEntryName.WasPassed() ? NS_Atomize(aEntryName.Value()) : nullptr; + RefPtr 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 { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("UserTiming"); + } + static void StreamJSONMarkerData( + baseprofiler::SpliceableJSONWriter& aWriter, + const ProfilerString16View& aName, bool aIsMeasure, + const Maybe& aStartMark, + const Maybe& aEndMark) { + aWriter.StringProperty("name", NS_ConvertUTF16toUTF8(aName)); + if (aIsMeasure) { + aWriter.StringProperty("entryType", "measure"); + } else { + aWriter.StringProperty("entryType", "mark"); + } + + if (aStartMark.isSome()) { + aWriter.StringProperty("startMark", NS_ConvertUTF16toUTF8(*aStartMark)); + } else { + aWriter.NullProperty("startMark"); + } + if (aEndMark.isSome()) { + aWriter.StringProperty("endMark", NS_ConvertUTF16toUTF8(*aEndMark)); + } else { + aWriter.NullProperty("endMark"); + } + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.SetAllLabels("{marker.data.name}"); + schema.AddStaticLabelValue("Marker", "UserTiming"); + schema.AddKeyLabelFormat("entryType", "Entry Type", MS::Format::String); + schema.AddKeyLabelFormatSearchable("name", "Name", MS::Format::String, + MS::Searchable::Searchable); + schema.AddKeyLabelFormat("startMark", "Start Mark", MS::Format::String); + schema.AddKeyLabelFormat("endMark", "End Mark", MS::Format::String); + schema.AddStaticLabelValue("Description", + "UserTimingMeasure is created using the DOM API " + "performance.measure()."); + return schema; + } +}; + +already_AddRefed Performance::Mark( + JSContext* aCx, const nsAString& aName, + const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) { + nsCOMPtr 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::Constructor(global, aName, aMarkOptions, aRv); + if (aRv.Failed()) { + return nullptr; + } + + InsertUserEntry(performanceMark); + + if (profiler_thread_is_being_profiled_for_markers()) { + Maybe 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& 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); + } + + AutoTArray, 1> arr; + Optional typeParam; + nsAutoString str; + str.AssignLiteral("mark"); + typeParam = &str; + GetEntriesByName(aName, typeParam, arr); + if (!arr.IsEmpty()) { + if (aReturnUnclamped) { + return arr.LastElement()->UnclampedStartTime(); + } + return arr.LastElement()->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& aEndMark, + const Maybe& 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& aStartMark, + const Maybe& 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; +#ifdef XP_WIN + s << "marker-" << GetCurrentProcessId() << ".txt"; +#else + s << "marker-" << getpid() << ".txt"; +#endif + return s.str(); +} + +// 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 aOptions, + Maybe aStartMark, const Optional& aEndMark) { + ErrorResult rv; + static FILE* markerFile = getenv("MOZ_USE_PERFORMANCE_MARKER_FILE") + ? fopen(GetMarkerFilename().c_str(), "w+") + : nullptr; + if (!markerFile) { + return; + } + + const DOMHighResTimeStamp unclampedStartTime = ResolveStartTimeForMeasure( + aStartMark, aOptions, rv, /* aReturnUnclamped */ true); + if (NS_WARN_IF(rv.Failed())) { + return; + } + const DOMHighResTimeStamp unclampedEndTime = + ResolveEndTimeForMeasure(aEndMark, aOptions, rv, /* aReturnUnclamped */ + true); + if (NS_WARN_IF(rv.Failed())) { + return; + } + + TimeStamp startTimeStamp = + CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedStartTime); + TimeStamp endTimeStamp = + CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedEndTime); + +#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.RawMachAbsoluteTimeValue(); + uint64_t rawEnd = endTimeStamp.RawMachAbsoluteTimeValue(); +#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: + // ` ` + // + // The timestamp value is OS specific. + fprintf(markerFile, "%" PRIu64 " %" PRIu64 " %s\n", rawStart, rawEnd, + NS_ConvertUTF16toUTF8(aName).get()); + fflush(markerFile); +} + +already_AddRefed Performance::Measure( + JSContext* aCx, const nsAString& aName, + const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions, + const Optional& aEndMark, ErrorResult& aRv) { + if (!GetParentObject()) { + aRv.ThrowInvalidStateError("Global object is unavailable"); + return nullptr; + } + + // Maybe is more readable than using the union type directly. + Maybe 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 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 detail(aCx); + if (options && !options->mDetail.isNullOrUndefined()) { + StructuredSerializeOptions serializeOptions; + JS::Rooted valueToClone(aCx, options->mDetail); + nsContentUtils::StructuredClone(aCx, GetParentObject(), valueToClone, + serializeOptions, &detail, aRv); + if (aRv.Failed()) { + return nullptr; + } + } else { + detail.setNull(); + } + + RefPtr performanceMeasure = new PerformanceMeasure( + GetParentObject(), aName, startTime, endTime, detail); + InsertUserEntry(performanceMeasure); + + MaybeEmitExternalProfilerMarker(aName, options, startMark, aEndMark); + + if (profiler_thread_is_being_profiled_for_markers()) { + const DOMHighResTimeStamp unclampedStartTime = ResolveStartTimeForMeasure( + startMark, options, aRv, /* aReturnUnclamped */ true); + const DOMHighResTimeStamp unclampedEndTime = + ResolveEndTimeForMeasure(aEndMark, options, aRv, /* aReturnUnclamped */ + true); + TimeStamp startTimeStamp = + CreationTimeStamp() + + TimeDuration::FromMilliseconds(unclampedStartTime); + TimeStamp endTimeStamp = + CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedEndTime); + + Maybe endMark; + if (aEndMark.WasPassed()) { + endMark.emplace(aEndMark.Value()); + } + + Maybe 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& 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(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 perfEntryEvent = + PerformanceEntryEvent::Constructor(this, u"performanceentry"_ns, init); + + nsCOMPtr 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(mPerformance); + performance->NotifyObservers(); + return NS_OK; + } + + nsresult Cancel() override { + mPerformance->CancelNotificationObservers(); + mPerformance = nullptr; + return NS_OK; + } + + private: + ~NotifyObserversTask() = default; + + RefPtr mPerformance; +}; + +void Performance::QueueNotificationObserversTask() { + if (!mPendingNotificationObserversTask) { + RunNotificationObserversTask(); + } +} + +void Performance::RunNotificationObserversTask() { + mPendingNotificationObserversTask = true; + nsCOMPtr task = new NotifyObserversTask(this); + nsresult rv; + if (GetOwnerGlobal()) { + rv = GetOwnerGlobal()->Dispatch(TaskCategory::Other, task.forget()); + } else { + rv = NS_DispatchToCurrentThread(task); + } + if (NS_WARN_IF(NS_FAILED(rv))) { + mPendingNotificationObserversTask = false; + } +} + +void Performance::QueueEntry(PerformanceEntry* aEntry) { + nsTObserverArray 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..d1f52e77d5 --- /dev/null +++ b/dom/performance/Performance.h @@ -0,0 +1,253 @@ +/* -*- 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 WorkerPrivate; +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 CreateForMainThread( + nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, + nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel); + + static already_AddRefed CreateForWorker( + WorkerPrivate* aWorkerPrivate); + + // This will return nullptr if called outside of a Window or Worker. + static already_AddRefed Get(JSContext* aCx, + nsIGlobalObject* aGlobal); + + JSObject* WrapObject(JSContext* cx, + JS::Handle aGivenProto) override; + + virtual void GetEntries(nsTArray>& aRetval); + + virtual void GetEntriesByType(const nsAString& aEntryType, + nsTArray>& aRetval); + + virtual void GetEntriesByTypeForObserver( + const nsAString& aEntryType, nsTArray>& aRetval); + + virtual void GetEntriesByName(const nsAString& aName, + const Optional& aEntryType, + nsTArray>& aRetval); + + virtual PerformanceStorage* AsPerformanceStorage() = 0; + + void ClearResourceTimings(); + + DOMHighResTimeStamp Now(); + + DOMHighResTimeStamp NowUnclamped() const; + + DOMHighResTimeStamp TimeOrigin(); + + already_AddRefed Mark( + JSContext* aCx, const nsAString& aName, + const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv); + + void ClearMarks(const Optional& aName); + + already_AddRefed Measure( + JSContext* aCx, const nsAString& aName, + const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions, + const Optional& aEndMark, ErrorResult& aRv); + + void ClearMeasures(const Optional& 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 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& 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> mObservers; + + protected: + static const uint64_t kDefaultResourceTimingBufferSize = 250; + + // When kDefaultResourceTimingBufferSize is increased or removed, these should + // be changed to use SegmentedVector + AutoTArray, kDefaultResourceTimingBufferSize> + mUserEntries; + AutoTArray, kDefaultResourceTimingBufferSize> + mResourceEntries; + AutoTArray, kDefaultResourceTimingBufferSize> + mSecondaryResourceEntries; + + uint64_t mResourceTimingBufferSize; + bool mPendingNotificationObserversTask; + + bool mPendingResourceTimingBufferFullEvent; + + RefPtr mPerformanceService; + + const RTPCallerType mRTPCallerType; + const bool mCrossOriginIsolated; + const bool mShouldResistFingerprinting; + + private: + MOZ_ALWAYS_INLINE bool CanAddResourceTimingEntry(); + void BufferEvent(); + void MaybeEmitExternalProfilerMarker( + const nsAString& aName, Maybe aOptions, + Maybe aStartMark, const Optional& 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& aEndMark, + const Maybe& aOptions, ErrorResult& aRv, + bool aReturnUnclamped); + DOMHighResTimeStamp ResolveStartTimeForMeasure( + const Maybe& aStartMark, + const Maybe& aOptions, ErrorResult& aRv, + bool aReturnUnclamped); +}; + +} // 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 mParent; + RefPtr mName; + RefPtr 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..456c0ad3c3 --- /dev/null +++ b/dom/performance/PerformanceEventTiming.cpp @@ -0,0 +1,206 @@ +/* -*- 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/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 + +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 aGivenProto) { + return PerformanceEventTiming_Binding::Wrap(cx, this, aGivenProto); +} + +already_AddRefed +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 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( + 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 = do_QueryReferent(mTarget); + if (!element) { + return nullptr; + } + + nsCOMPtr 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 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> { + public: + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PerformanceEventTiming, + PerformanceEntry) + + static already_AddRefed TryGenerateEventTiming( + const EventTarget* aTarget, const WidgetEvent* aEvent); + + already_AddRefed Clone() { + RefPtr eventTiming = + new PerformanceEventTiming(*this); + return eventTiming.forget(); + } + + JSObject* WrapObject(JSContext* cx, + JS::Handle 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 mPerformance; + + DOMHighResTimeStamp mProcessingStart; + mutable Maybe mCachedProcessingStart; + + DOMHighResTimeStamp mProcessingEnd; + mutable Maybe mCachedProcessingEnd; + + nsWeakPtr mTarget; + + DOMHighResTimeStamp mStartTime; + mutable Maybe mCachedStartTime; + + DOMHighResTimeStamp mDuration; + mutable Maybe 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..d0e942ab5a --- /dev/null +++ b/dom/performance/PerformanceMainThread.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 "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 "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" + +namespace mozilla::dom { + +namespace { + +void GetURLSpecFromChannel(nsITimedChannel* aChannel, nsAString& aSpec) { + aSpec.AssignLiteral("document"); + + nsCOMPtr channel = do_QueryInterface(aChannel); + if (!channel) { + return; + } + + nsCOMPtr 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, mFirstInputEvent, + mPendingPointerDown, + mPendingEventTimingEntries, mEventCounts) + 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, mFirstInputEvent, + mPendingPointerDown, + mPendingEventTimingEntries, mEventCounts) +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(); +} + +PerformanceMainThread::~PerformanceMainThread() { + mozilla::DropJSObjects(this); +} + +void PerformanceMainThread::GetMozMemory(JSContext* aCx, + JS::MutableHandle aObj) { + if (!mMozMemory) { + JS::Rooted mozMemoryObj(aCx, JS_NewPlainObject(aCx)); + JS::Rooted 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 = 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::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&& aData) { + AddRawEntry(std::move(aData), initiatorType, entryName); +} + +void PerformanceMainThread::AddRawEntry(UniquePtr aData, + const nsAString& aInitiatorType, + const nsAString& aEntryName) { + // The PerformanceResourceTiming object will use the PerformanceTimingData + // object to get all the required timings. + auto entry = + MakeRefPtr(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(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::DispatchPendingEventTimingEntries() { + DOMHighResTimeStamp renderingTime = NowUnclamped(); + + while (!mPendingEventTimingEntries.isEmpty()) { + RefPtr 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); + mHasDispatchedInputEvent = true; + } + break; + } + case eMouseClick: + case eKeyDown: + case eMouseDown: { + mFirstInputEvent = entry->Clone(); + mFirstInputEvent->SetEntryType(u"first-input"_ns); + QueueEntry(mFirstInputEvent); + mHasDispatchedInputEvent = true; + 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 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(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 timing( + new PerformanceTimingData(mChannel, nullptr, 0)); + + nsCOMPtr 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 httpChannel = do_QueryInterface(mChannel); + if (httpChannel) { + mDocEntry->UpdatePropertiesFromHttpChannel(httpChannel, mChannel); + } +} + +void PerformanceMainThread::QueueNavigationTimingEntry() { + if (!mDocEntry) { + return; + } + + UpdateNavigationTimingEntry(); + + QueueEntry(mDocEntry); +} + +EventCounts* PerformanceMainThread::EventCounts() { + MOZ_ASSERT(StaticPrefs::dom_enable_event_timing()); + return mEventCounts; +} + +void PerformanceMainThread::GetEntries( + nsTArray>& 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>& aRetval) { + RefPtr 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>& aRetval) { + if (aEntryType.EqualsLiteral("event")) { + aRetval.AppendElements(mEventTimingEntries); + return; + } + return GetEntriesByType(aEntryType, aRetval); +} + +void PerformanceMainThread::GetEntriesByName( + const nsAString& aName, const Optional& aEntryType, + nsTArray>& 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->AsInnerWindow()->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; +} +} // namespace mozilla::dom diff --git a/dom/performance/PerformanceMainThread.h b/dom/performance/PerformanceMainThread.h new file mode 100644 index 0000000000..8a38a5faec --- /dev/null +++ b/dom/performance/PerformanceMainThread.h @@ -0,0 +1,141 @@ +/* -*- 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" + +namespace mozilla::dom { + +class PerformanceNavigationTiming; +class PerformanceEventTiming; + +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&& aData) override; + + // aPerformanceTimingData must be non-null. + void AddRawEntry(UniquePtr aPerformanceTimingData, + const nsAString& aInitiatorType, + const nsAString& aEntryName); + virtual void SetFCPTimingEntry(PerformancePaintTiming* aEntry) override; + + void InsertEventTimingEntry(PerformanceEventTiming*) override; + void BufferEventTimingEntryIfNeeded(PerformanceEventTiming*) override; + void DispatchPendingEventTimingEntries() override; + + TimeStamp CreationTimeStamp() const override; + + DOMHighResTimeStamp CreationTime() const override; + + virtual void GetMozMemory(JSContext* aCx, + JS::MutableHandle 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>& aRetval) override; + + // Return entries which qualify availableFromTimeline boolean check + virtual void GetEntriesByType( + const nsAString& aEntryType, + nsTArray>& 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>& aRetval) override; + virtual void GetEntriesByName( + const nsAString& aName, const Optional& aEntryType, + nsTArray>& aRetval) override; + + void UpdateNavigationTimingEntry() override; + void QueueNavigationTimingEntry() override; + + 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; + + class EventCounts* EventCounts() override; + + bool IsGlobalObjectWindow() const override { return true; }; + + protected: + ~PerformanceMainThread(); + + void CreateNavigationTimingEntry(); + + void InsertUserEntry(PerformanceEntry* aEntry) override; + + DOMHighResTimeStamp GetPerformanceTimingFromString( + const nsAString& aTimingName) override; + + void DispatchBufferFullEvent() override; + + RefPtr mDocEntry; + RefPtr mDOMTiming; + nsCOMPtr mChannel; + RefPtr mTiming; + RefPtr mNavigation; + RefPtr mFCPTiming; + JS::Heap mMozMemory; + + nsTArray> mEventTimingEntries; + + AutoCleanLinkedList> + mPendingEventTimingEntries; + bool mHasDispatchedInputEvent = false; + + RefPtr mFirstInputEvent; + RefPtr mPendingPointerDown; + + private: + bool mHasQueuedRefreshdriverObserver = false; + + RefPtr mEventCounts; + void IncEventCount(const nsAtom* aType); + + PresShell* GetPresShell(); +}; + +} // 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& aDetail, + DOMHighResTimeStamp aUnclampedStartTime) + : PerformanceEntry(aParent, aName, u"mark"_ns), + mStartTime(aStartTime), + mDetail(aDetail), + mUnclampedStartTime(aUnclampedStartTime) { + mozilla::HoldJSObjects(this); +} + +already_AddRefed PerformanceMark::Constructor( + const GlobalObject& aGlobal, const nsAString& aMarkName, + const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) { + const nsCOMPtr global = + do_QueryInterface(aGlobal.GetAsSupports()); + return PerformanceMark::Constructor(aGlobal.Context(), global, aMarkName, + aMarkOptions, aRv); +} + +already_AddRefed PerformanceMark::Constructor( + JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aMarkName, + const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) { + RefPtr 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 detail(aCx); + if (aMarkOptions.mDetail.isNullOrUndefined()) { + detail.setNull(); + } else { + StructuredSerializeOptions serializeOptions; + JS::Rooted 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 aGivenProto) { + return PerformanceMark_Binding::Wrap(aCx, this, aGivenProto); +} + +void PerformanceMark::GetDetail(JSContext* aCx, + JS::MutableHandle 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& aDetail, + DOMHighResTimeStamp aUnclampedStartTime); + + public: + static already_AddRefed Constructor( + const GlobalObject& aGlobal, const nsAString& aMarkName, + const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv); + + static already_AddRefed Constructor( + JSContext* aCx, nsIGlobalObject* aGlobal, const nsAString& aMarkName, + const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle 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 aRetval); + + size_t SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const override; + + protected: + virtual ~PerformanceMark(); + DOMHighResTimeStamp mStartTime; + + private: + JS::Heap 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& 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 aGivenProto) { + return PerformanceMeasure_Binding::Wrap(aCx, this, aGivenProto); +} + +void PerformanceMeasure::GetDetail(JSContext* aCx, + JS::MutableHandle 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& aDetail); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + virtual DOMHighResTimeStamp StartTime() const override { return mStartTime; } + + virtual DOMHighResTimeStamp Duration() const override { return mDuration; } + + void GetDetail(JSContext* aCx, JS::MutableHandle aRetval); + + size_t SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const override; + + protected: + virtual ~PerformanceMeasure(); + DOMHighResTimeStamp mStartTime; + DOMHighResTimeStamp mDuration; + + private: + JS::Heap 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 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 aGivenProto) override; + + // PerformanceNavigation WebIDL methods + uint16_t Type() const { return GetDOMTiming()->GetType(); } + + uint16_t RedirectCount() const; + + private: + ~PerformanceNavigation(); + RefPtr 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..909919624a --- /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 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( + Maybe& aSubjectPrincipal) const { + return PerformanceResourceTiming::RedirectStart( + aSubjectPrincipal, true /* aEnsureSameOriginAndIgnoreTAO */); +} + +DOMHighResTimeStamp PerformanceNavigationTiming::RedirectEnd( + Maybe& 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..99ee12d2cc --- /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 +#include +#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&& 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 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( + Maybe& aSubjectPrincipal) const override; + DOMHighResTimeStamp RedirectEnd( + Maybe& 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..e132b70860 --- /dev/null +++ b/dom/performance/PerformanceObserver.cpp @@ -0,0 +1,367 @@ +/* -*- 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 "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::Constructor( + const GlobalObject& aGlobal, PerformanceObserverCallback& aCb, + ErrorResult& aRv) { + if (NS_IsMainThread()) { + nsCOMPtr ownerWindow = + do_QueryInterface(aGlobal.GetAsSupports()); + if (!ownerWindow) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr observer = + new PerformanceObserver(ownerWindow, aCb); + return observer.forget(); + } + + JSContext* cx = aGlobal.Context(); + WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(cx); + MOZ_ASSERT(workerPrivate); + + RefPtr observer = + new PerformanceObserver(workerPrivate, aCb); + return observer.forget(); +} + +JSObject* PerformanceObserver::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return PerformanceObserver_Binding::Wrap(aCx, this, aGivenProto); +} + +void PerformanceObserver::Notify() { + if (mQueuedEntries.IsEmpty()) { + return; + } + RefPtr list = + new PerformanceObserverEntryList(this, mQueuedEntries); + + mQueuedEntries.Clear(); + + ErrorResult rv; + RefPtr 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 params; + params.AppendElement(aInvalidTypes); + WorkerPrivate::ReportErrorToConsole(msgId, params); + } else { + nsCOMPtr ownerWindow = do_QueryInterface(mOwner); + Document* document = ownerWindow->GetExtantDoc(); + AutoTArray params = {aInvalidTypes}; + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, + document, nsContentUtils::eDOM_PROPERTIES, + msgId, params); + } +} + +void PerformanceObserver::Observe(const PerformanceObserverInit& aOptions, + ErrorResult& aRv) { + const Optional>& maybeEntryTypes = aOptions.mEntryTypes; + const Optional& maybeType = aOptions.mType; + const Optional& 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& entryTypes = maybeEntryTypes.Value(); + + if (entryTypes.IsEmpty()) { + return; + } + + /* 3.3.1.5.2 */ + nsTArray validEntryTypes; + + if (StaticPrefs::dom_enable_event_timing()) { + for (const nsLiteralString& name : kValidEventTimingNames) { + if (entryTypes.Contains(name) && !validEntryTypes.Contains(name)) { + validEntryTypes.AppendElement(name); + } + } + } + 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(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 (!typeValid) { + ReportUnsupportedTypesErrorToConsole( + NS_IsMainThread(), UnsupportedEntryTypesIgnoredMsgId, type); + return; + } + + /* 3.3.1.6.4, 3.3.1.6.4 */ + bool didUpdateOptionsList = false; + nsTArray 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> 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 aObject) { + nsTArray validTypes; + JS::Rooted val(aGlobal.Context()); + + if (StaticPrefs::dom_enable_event_timing()) { + for (const nsLiteralString& name : kValidEventTimingNames) { + validTypes.AppendElement(name); + } + } + 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>& 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 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 aGivenProto) override; + + nsISupports* GetParentObject() const { return mOwner; } + + void Observe(const PerformanceObserverInit& aOptions, ErrorResult& aRv); + static void GetSupportedEntryTypes(const GlobalObject& aGlobal, + JS::MutableHandle aObject); + + void Disconnect(); + + void TakeRecords(nsTArray>& 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 mOwner; + RefPtr mCallback; + RefPtr mPerformance; + nsTArray mEntryTypes; + nsTArray mOptions; + enum { + ObserverTypeUndefined, + ObserverTypeSingle, + ObserverTypeMultiple, + } mObserverType; + /* + * This is also known as registered, in the spec. + */ + bool mConnected; + nsTArray> 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 aGivenProto) { + return PerformanceObserverEntryList_Binding::Wrap(aCx, this, aGivenProto); +} + +void PerformanceObserverEntryList::GetEntries( + const PerformanceEntryFilterOptions& aFilter, + nsTArray>& aRetval) { + aRetval.Clear(); + RefPtr name = + aFilter.mName.WasPassed() ? NS_Atomize(aFilter.mName.Value()) : nullptr; + RefPtr entryType = aFilter.mEntryType.WasPassed() + ? NS_Atomize(aFilter.mEntryType.Value()) + : nullptr; + for (const RefPtr& 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>& aRetval) { + aRetval.Clear(); + RefPtr entryType = NS_Atomize(aEntryType); + for (const RefPtr& entry : mEntries) { + if (entry->GetEntryType() == entryType) { + aRetval.AppendElement(entry); + } + } + aRetval.Sort(PerformanceEntryComparator()); +} + +void PerformanceObserverEntryList::GetEntriesByName( + const nsAString& aName, const Optional& aEntryType, + nsTArray>& aRetval) { + aRetval.Clear(); + RefPtr name = NS_Atomize(aName); + RefPtr entryType = + aEntryType.WasPassed() ? NS_Atomize(aEntryType.Value()) : nullptr; + for (const RefPtr& 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 +class Optional; + +class PerformanceObserverEntryList final : public nsISupports, + public nsWrapperCache { + ~PerformanceObserverEntryList(); + + public: + PerformanceObserverEntryList( + nsISupports* aOwner, const nsTArray>& aEntries) + : mOwner(aOwner), mEntries(aEntries.Clone()) {} + + nsISupports* GetParentObject() const { return mOwner; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PerformanceObserverEntryList) + + void GetEntries(const PerformanceEntryFilterOptions& aFilter, + nsTArray>& aRetval); + void GetEntriesByType(const nsAString& aEntryType, + nsTArray>& aRetval); + void GetEntriesByName(const nsAString& aName, + const Optional& aEntryType, + nsTArray>& aRetval); + + private: + nsCOMPtr mOwner; + nsTArray> 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 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 aGivenProto) override; + + DOMHighResTimeStamp StartTime() const override; + + size_t SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const override; + + private: + ~PerformancePaintTiming(); + RefPtr mPerformance; + + const TimeStamp mRawStartTime; + mutable Maybe 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..40a0b77382 --- /dev/null +++ b/dom/performance/PerformanceResourceTiming.cpp @@ -0,0 +1,134 @@ +/* -*- 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&& 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 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>& aRetval, + Maybe& aSubjectPrincipal) { + aRetval.Clear(); + if (!TimingAllowedForCaller(aSubjectPrincipal)) { + return; + } + + nsTArray> serverTimingArray = + mTimingData->GetServerTiming(); + uint32_t length = serverTimingArray.Length(); + for (uint32_t index = 0; index < length; ++index) { + nsCOMPtr serverTiming = serverTimingArray.ElementAt(index); + MOZ_ASSERT(serverTiming); + + aRetval.AppendElement( + new PerformanceServerTiming(GetParentObject(), serverTiming)); + } +} + +bool PerformanceResourceTiming::TimingAllowedForCaller( + Maybe& aCaller) const { + if (mTimingData->TimingAllowed()) { + return true; + } + + // Check if the addon has permission to access the cross-origin resource. + return mOriginalURI && aCaller.isSome() && + BasePrincipal::Cast(aCaller.value())->AddonAllowsLoad(mOriginalURI); +} + +bool PerformanceResourceTiming::ReportRedirectForCaller( + Maybe& aCaller, bool aEnsureSameOriginAndIgnoreTAO) const { + if (mTimingData->ShouldReportCrossOriginRedirect( + aEnsureSameOriginAndIgnoreTAO)) { + return true; + } + + // Only report cross-origin redirect if the addon has permission. + return aCaller.isSome() && + BasePrincipal::Cast(aCaller.value()) + ->AddonHasPermission(nsGkAtoms::all_urlsPermission); +} diff --git a/dom/performance/PerformanceResourceTiming.h b/dom/performance/PerformanceResourceTiming.h new file mode 100644 index 0000000000..75fa593b1b --- /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(Maybe& 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(Maybe& 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&& aPerformanceTimingData, + Performance* aPerformance, const nsAString& aName); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle 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(Maybe& 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( + Maybe& aSubjectPrincipal) const { + return RedirectStart(aSubjectPrincipal, + false /* aEnsureSameOriginAndIgnoreTAO */); + } + + DOMHighResTimeStamp RedirectEnd(Maybe& 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( + Maybe& 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>& aRetval, + Maybe& 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(Maybe& aCaller) const; + + // Check if cross-origin redirects should be reported to the caller. + bool ReportRedirectForCaller(Maybe& aCaller, + bool aEnsureSameOriginAndIgnoreTAO) const; + + nsString mInitiatorType; + const UniquePtr mTimingData; // always non-null + RefPtr mPerformance; + + // The same initial requested URI as the `name` attribute. + nsCOMPtr mOriginalURI; + + private: + mutable Maybe 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 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 aGivenProto) override; + + nsISupports* GetParentObject() const { return mParent; } + + void GetName(nsAString& aName) const; + + DOMHighResTimeStamp Duration() const; + + void GetDescription(nsAString& aDescription) const; + + private: + ~PerformanceServerTiming() = default; + + nsCOMPtr mParent; + nsCOMPtr 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 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&& 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..6a6fbea6d0 --- /dev/null +++ b/dom/performance/PerformanceStorageWorker.cpp @@ -0,0 +1,184 @@ +/* -*- 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&& aData, + const nsAString& aInitiatorType, + const nsAString& aEntryName) + : mData(std::move(aData)), + mInitiatorType(aInitiatorType), + mEntryName(aEntryName) { + MOZ_RELEASE_ASSERT(mData); + } + + UniquePtr 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&& aData) + : WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount), + 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 WorkerRunnable::Cancel(); + } + + bool PreDispatch(WorkerPrivate* aWorkerPrivate) override { return true; } + + void PostDispatch(WorkerPrivate* aWorkerPrivate, + bool aDispatchResult) override {} + + private: + RefPtr mStorage; + UniquePtr mData; +}; + +} // namespace + +/* static */ +already_AddRefed PerformanceStorageWorker::Create( + WorkerPrivate* aWorkerPrivate) { + MOZ_ASSERT(aWorkerPrivate); + aWorkerPrivate->AssertIsOnWorkerThread(); + + RefPtr 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::Create(aTimedChannel, aChannel, 0, initiatorType, + entryName)); + if (!performanceTimingData) { + return; + } + + UniquePtr data(new PerformanceProxyData( + std::move(performanceTimingData), initiatorType, entryName)); + + RefPtr r = + new PerformanceEntryAdder(workerPrivate, this, std::move(data)); + Unused << NS_WARN_IF(!r->Dispatch()); +} + +void PerformanceStorageWorker::AddEntry( + const nsString& aEntryName, const nsString& aInitiatorType, + UniquePtr&& aData) { + MOZ_ASSERT(!NS_IsMainThread()); + if (!aData) { + return; + } + + UniquePtr data = MakeUnique( + 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&& aData) { + RefPtr performance; + UniquePtr 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 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 Create( + WorkerPrivate* aWorkerPrivate); + + void ShutdownOnWorker(); + + void AddEntry(nsIHttpChannel* aChannel, + nsITimedChannel* aTimedChannel) override; + void AddEntry(const nsString& aEntryName, const nsString& aInitiatorType, + UniquePtr&& aData) override; + void AddEntryOnWorker(UniquePtr&& aData); + + private: + PerformanceStorageWorker(); + ~PerformanceStorageWorker(); + + Mutex mMutex; + + // Protected by mutex. + // Created and released on worker-thread. Used also on main-thread. + RefPtr 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..67e1ba5c74 --- /dev/null +++ b/dom/performance/PerformanceTiming.cpp @@ -0,0 +1,676 @@ +/* -*- 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 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()) { + Telemetry::Accumulate(Telemetry::TIME_TO_RESPONSE_START_MS, + 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 uri; + if (aHttpChannel) { + aHttpChannel->GetURI(getter_AddRefs(uri)); + } else { + nsCOMPtr 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 timing = new nsServerTiming(); + timing->SetName(serverTimingData.name()); + timing->SetDuration(serverTimingData.duration()); + timing->SetDescription(serverTimingData.description()); + mServerTiming.AppendElement(timing); + } +} + +IPCPerformanceTimingData PerformanceTimingData::ToIPC() { + nsTArray 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(mTimingData->FetchStartHighRes(mPerformance)); +} + +bool PerformanceTimingData::CheckAllowedOrigin(nsIHttpChannel* aResourceChannel, + nsITimedChannel* aChannel) { + if (!IsInitialized()) { + return false; + } + + // Check that the current document passes the ckeck. + nsCOMPtr loadInfo = aResourceChannel->LoadInfo(); + + // TYPE_DOCUMENT loads have no loadingPrincipal. + if (loadInfo->GetExternalContentPolicyType() == + ExtContentPolicy::TYPE_DOCUMENT) { + return true; + } + + nsCOMPtr 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( + 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(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( + 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( + 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(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( + 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(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(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(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(mTimingData->ResponseEndHighRes(mPerformance)); +} + +JSObject* PerformanceTiming::WrapObject(JSContext* cx, + JS::Handle aGivenProto) { + return PerformanceTiming_Binding::Wrap(cx, this, aGivenProto); +} + +bool PerformanceTiming::IsTopLevelContentDocument() const { + nsCOMPtr document = mPerformance->GetDocumentIfCurrent(); + if (!document) { + return false; + } + + if (BrowsingContext* bc = document->GetBrowsingContext()) { + return bc->IsTopContent(); + } + return false; +} + +nsTArray> PerformanceTimingData::GetServerTiming() { + if (!StaticPrefs::dom_enable_performance() || !IsInitialized() || + !TimingAllowed()) { + return nsTArray>(); + } + + 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> 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> 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 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 mPerformance; + + UniquePtr mTimingData; +}; + +} // namespace mozilla::dom + +namespace mozilla::ipc { + +template <> +struct IPDLParamTraits { + 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 { + 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* 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 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..65dc474d45 --- /dev/null +++ b/dom/performance/PerformanceWorker.cpp @@ -0,0 +1,64 @@ +/* -*- 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(WorkerPrivate* aWorkerPrivate) + : Performance(aWorkerPrivate->GlobalScope()), + mWorkerPrivate(aWorkerPrivate) { + mWorkerPrivate->AssertIsOnWorkerThread(); +} + +PerformanceWorker::~PerformanceWorker() { + if (mWorkerPrivate) { + mWorkerPrivate->AssertIsOnWorkerThread(); + } +} + +void PerformanceWorker::InsertUserEntry(PerformanceEntry* aEntry) { + if (StaticPrefs::dom_performance_enable_user_timing_logging()) { + nsAutoCString uri; + nsCOMPtr scriptURI = mWorkerPrivate->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 { + MOZ_DIAGNOSTIC_ASSERT(mWorkerPrivate); + if (mWorkerPrivate) { + return mWorkerPrivate->CreationTimeStamp(); + } + return TimeStamp(); +} + +DOMHighResTimeStamp PerformanceWorker::CreationTime() const { + MOZ_DIAGNOSTIC_ASSERT(mWorkerPrivate); + if (mWorkerPrivate) { + return mWorkerPrivate->CreationTime(); + } + return DOMHighResTimeStamp(); +} + +uint64_t PerformanceWorker::GetRandomTimelineSeed() { + MOZ_DIAGNOSTIC_ASSERT(mWorkerPrivate); + if (mWorkerPrivate) { + return mWorkerPrivate->GetRandomTimelineSeed(); + } + return 0; +} + +void PerformanceWorker::NoteShuttingDown() { mWorkerPrivate = nullptr; } + +} // namespace mozilla::dom diff --git a/dom/performance/PerformanceWorker.h b/dom/performance/PerformanceWorker.h new file mode 100644 index 0000000000..37c23a3a7a --- /dev/null +++ b/dom/performance/PerformanceWorker.h @@ -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/. */ + +#ifndef mozilla_dom_PerformanceWorker_h +#define mozilla_dom_PerformanceWorker_h + +#include "Performance.h" +#include "mozilla/dom/WorkerPrivate.h" + +namespace mozilla::dom { + +class PerformanceWorker final : public Performance { + public: + explicit PerformanceWorker(WorkerPrivate* aWorkerPrivate); + + 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 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"); + } + + void NoteShuttingDown(); + + protected: + ~PerformanceWorker(); + + void InsertUserEntry(PerformanceEntry* aEntry) override; + + void DispatchBufferFullEvent() override { + // Nothing to do here. See bug 1432758. + } + + private: + CheckedUnsafePtr mWorkerPrivate; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_PerformanceWorker_h diff --git a/dom/performance/moz.build b/dom/performance/moz.build new file mode 100644 index 0000000000..23e77c1c29 --- /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.ini"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/performance/tests/empty.js b/dom/performance/tests/empty.js new file mode 100644 index 0000000000..3b44754e30 --- /dev/null +++ b/dom/performance/tests/empty.js @@ -0,0 +1 @@ +/* Nothing here */ diff --git a/dom/performance/tests/logo.png b/dom/performance/tests/logo.png new file mode 100644 index 0000000000..a05926bcd7 Binary files /dev/null and b/dom/performance/tests/logo.png differ diff --git a/dom/performance/tests/mochitest.ini b/dom/performance/tests/mochitest.ini new file mode 100644 index 0000000000..9fbdac9efe --- /dev/null +++ b/dom/performance/tests/mochitest.ini @@ -0,0 +1,44 @@ +[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_observer.html] +[test_performance_user_timing.html] +[test_performance_user_timing_dying_global.html] +[test_performance_navigation_timing.html] +[test_performance_paint_timing.html] +support-files = + test_performance_paint_timing_helper.html + logo.png +[test_performance_paint_observer.html] +support-files = + test_performance_paint_observer_helper.html + logo.png +[test_worker_user_timing.html] +[test_worker_observer.html] +[test_sharedWorker_performance_user_timing.html] +skip-if = true # Bug 1571904 +[test_worker_performance_now.html] +[test_timeOrigin.html] +skip-if = toolkit == 'android' # Bug 1525959 +[test_worker_performance_entries.html] +skip-if = + toolkit == 'android' # Bug 1525959 + http3 +[test_performance_timing_json.html] +[test_performance_server_timing.html] +scheme = https +skip-if = + http3 +[test_performance_server_timing_plain_http.html] +skip-if = + http3 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 @@ + + + + + Test for Bug 1462891 + + + + + Mozilla Bug 1462891 - Navigation Timing API +
+
+
+            
+        
+ + 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 @@ + + + + + +Test for performance observer + + + + +
+ + + + 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 @@ + + + + + + Test for Bug 1518999 (Observer API) + + + + + + Mozilla + Bug 1518999 - Paint Timing API For Observers +

+ + + 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 @@ + + + + + + + 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 @@ + + + + + + Test for Bug 1518999 + + + + + + Mozilla + Bug 1518999 - Paint Timing API +

+ + + 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 @@ + + + + + + Test for Bug 1518999 + + + +
+
+
+ +
+ + + 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 @@ + + + + + +Test for PerformanceServerTiming + + + + +
+ + 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 @@ + + + + + +Plain HTTP Test for PerformanceServerTiming + + + + +
+ + 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 @@ + + + + + + Test for Bug 1375829 + + + + + +Mozilla Bug 1375829 +

+ +
+
+ + 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 @@ + + + + + Test for Bug 782751 + + + + + + Mozilla Bug 782751 - User Timing API +
+
+
+            
+        
+ + 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 @@ + + + + Test for User Timing APIs on dying globals + + + + + + + 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 @@ + + + + + Test for worker performance timing API + + + + + + + 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 @@ + + + + Test for performance.timeOrigin + + + + + + + + + + + + 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 @@ + + + + + +Test for performance observer in worker + + + + +
+ + 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 @@ + + + + + PerformanceResouceTiming in workers + + + + + + + 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 @@ + + + + + Validate Interfaces Exposed to Workers + + + + + + + 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 @@ + + + + + Test for worker performance timing API + + + + + + + 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" }); -- cgit v1.2.3