/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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 "GeckoProfiler.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/ErrorResult.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/dom/MessagePortBinding.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/Preferences.h" #include "mozilla/TimerClamping.h" #include "WorkerPrivate.h" #include "WorkerRunnable.h" #include "WorkerScope.h" #define PERFLOG(msg, ...) printf_stderr(msg, ##__VA_ARGS__) namespace mozilla { namespace dom { using namespace workers; namespace { class PrefEnabledRunnable final : public WorkerCheckAPIExposureOnMainThreadRunnable { public: PrefEnabledRunnable(WorkerPrivate* aWorkerPrivate, const nsCString& aPrefName) : WorkerCheckAPIExposureOnMainThreadRunnable(aWorkerPrivate) , mEnabled(false) , mPrefName(aPrefName) { } bool MainThreadRun() override { MOZ_ASSERT(NS_IsMainThread()); mEnabled = Preferences::GetBool(mPrefName.get(), false); return true; } bool IsEnabled() const { return mEnabled; } private: bool mEnabled; nsCString mPrefName; }; } // anonymous namespace 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); NS_IMPL_ADDREF_INHERITED(Performance, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(Performance, DOMEventTargetHelper) /* static */ already_AddRefed Performance::CreateForMainThread(nsPIDOMWindowInner* aWindow, nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel) { MOZ_ASSERT(NS_IsMainThread()); RefPtr performance = new PerformanceMainThread(aWindow, aDOMTiming, aChannel); return performance.forget(); } /* static */ already_AddRefed Performance::CreateForWorker(workers::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() : mResourceTimingBufferSize(kDefaultResourceTimingBufferSize) , mPendingNotificationObserversTask(false) { MOZ_ASSERT(!NS_IsMainThread()); } Performance::Performance(nsPIDOMWindowInner* aWindow) : DOMEventTargetHelper(aWindow) , mResourceTimingBufferSize(kDefaultResourceTimingBufferSize) , mPendingNotificationObserversTask(false) { MOZ_ASSERT(NS_IsMainThread()); } Performance::~Performance() {} DOMHighResTimeStamp Performance::Now() const { TimeDuration duration = TimeStamp::Now() - CreationTimeStamp(); return RoundTime(duration.ToMilliseconds()); } DOMHighResTimeStamp Performance::TimeOrigin() { if (!mPerformanceService) { mPerformanceService = PerformanceService::GetOrCreate(); } MOZ_ASSERT(mPerformanceService); return mPerformanceService->TimeOrigin(CreationTimeStamp()); } JSObject* Performance::WrapObject(JSContext* aCx, JS::Handle aGivenProto) { return PerformanceBinding::Wrap(aCx, this, aGivenProto); } void Performance::GetEntries(nsTArray>& aRetval) { aRetval = mResourceEntries; aRetval.AppendElements(mUserEntries); aRetval.Sort(PerformanceEntryComparator()); } void Performance::GetEntriesByType(const nsAString& aEntryType, nsTArray>& aRetval) { if (aEntryType.EqualsLiteral("resource")) { aRetval = mResourceEntries; return; } aRetval.Clear(); if (aEntryType.EqualsLiteral("mark") || aEntryType.EqualsLiteral("measure")) { for (PerformanceEntry* entry : mUserEntries) { if (entry->GetEntryType().Equals(aEntryType)) { aRetval.AppendElement(entry); } } } } void Performance::GetEntriesByName(const nsAString& aName, const Optional& aEntryType, nsTArray>& aRetval) { aRetval.Clear(); for (PerformanceEntry* entry : mResourceEntries) { if (entry->GetName().Equals(aName) && (!aEntryType.WasPassed() || entry->GetEntryType().Equals(aEntryType.Value()))) { aRetval.AppendElement(entry); } } for (PerformanceEntry* entry : mUserEntries) { if (entry->GetName().Equals(aName) && (!aEntryType.WasPassed() || entry->GetEntryType().Equals(aEntryType.Value()))) { aRetval.AppendElement(entry); } } aRetval.Sort(PerformanceEntryComparator()); } void Performance::ClearUserEntries(const Optional& aEntryName, const nsAString& aEntryType) { for (uint32_t i = 0; i < mUserEntries.Length();) { if ((!aEntryName.WasPassed() || mUserEntries[i]->GetName().Equals(aEntryName.Value())) && (aEntryType.IsEmpty() || mUserEntries[i]->GetEntryType().Equals(aEntryType))) { mUserEntries.RemoveElementAt(i); } else { ++i; } } } void Performance::ClearResourceTimings() { MOZ_ASSERT(NS_IsMainThread()); mResourceEntries.Clear(); } DOMHighResTimeStamp Performance::RoundTime(double aTime) const { // Round down to the nearest 2ms, because if the timer is too accurate people // can do nasty timing attacks with it. const double maxResolutionMs = 2; return floor(aTime / maxResolutionMs) * maxResolutionMs; } already_AddRefed Performance::Mark( JSContext* aCx, const nsAString& aName, const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) { // Don't add the entry if the buffer is full. XXX should be removed by bug 1159003. if (mUserEntries.Length() >= mResourceTimingBufferSize) { return nullptr; } nsCOMPtr parent = GetParentObject(); if (!parent || parent->IsDying() || !parent->GetGlobalJSObject()) { aRv.Throw(NS_ERROR_DOM_UT_UNAVAILABLE_GLOBAL_OBJECT); return nullptr; } GlobalObject global(aCx, parent->GetGlobalJSObject()); if (global.Failed()) { aRv.Throw(NS_ERROR_DOM_UT_UNAVAILABLE_GLOBAL_OBJECT); return nullptr; } RefPtr performanceMark = PerformanceMark::Constructor(global, aName, aMarkOptions, aRv); if (aRv.Failed()) { return nullptr; } InsertUserEntry(performanceMark); if (profiler_is_active()) { PROFILER_MARKER(NS_ConvertUTF16toUTF8(aName).get()); } return performanceMark.forget(); } void Performance::ClearMarks(const Optional& aName) { ClearUserEntries(aName, NS_LITERAL_STRING("mark")); } // 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) { 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()) { return arr.LastElement()->StartTime(); } aRv.Throw(NS_ERROR_DOM_UT_UNKNOWN_MARK_NAME); return 0; } DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithDOMHighResTimeStamp( const ResolveTimestampAttribute aAttribute, const DOMHighResTimeStamp aTimestamp, ErrorResult& aRv) { if (aTimestamp < 0) { nsAutoString attributeName; switch (aAttribute) { case ResolveTimestampAttribute::Start: attributeName = NS_LITERAL_STRING("start"); break; case ResolveTimestampAttribute::End: attributeName = NS_LITERAL_STRING("end"); break; case ResolveTimestampAttribute::Duration: attributeName = NS_LITERAL_STRING("duration"); break; } aRv.ThrowTypeError(attributeName); } return aTimestamp; } DOMHighResTimeStamp Performance::ConvertMarkToTimestamp( const ResolveTimestampAttribute aAttribute, const OwningStringOrDouble& aMarkNameOrTimestamp, ErrorResult& aRv) { if (aMarkNameOrTimestamp.IsString()) { return ConvertMarkToTimestampWithString(aMarkNameOrTimestamp.GetAsString(), aRv); } return ConvertMarkToTimestampWithDOMHighResTimeStamp( aAttribute, aMarkNameOrTimestamp.GetAsDouble(), aRv); } DOMHighResTimeStamp Performance::ConvertNameToTimestamp(const nsAString& aName, ErrorResult& aRv) { if (!IsGlobalObjectWindow()) { aRv.ThrowTypeError(aName); 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(NS_LITERAL_STRING("navigationStart")); const DOMHighResTimeStamp endTime = GetPerformanceTimingFromString(aName); MOZ_ASSERT(endTime >= 0); if (endTime == 0) { aRv.Throw(NS_ERROR_DOM_UT_UNAVAILABLE_ATTR); return 0; } return endTime - startTime; } DOMHighResTimeStamp Performance::ResolveEndTimeForMeasure( const Optional& aEndMark, const Maybe& aOptions, ErrorResult& aRv) { DOMHighResTimeStamp endTime; if (aEndMark.WasPassed()) { endTime = ConvertMarkToTimestampWithString(aEndMark.Value(), aRv); } else if (aOptions && aOptions->mEnd.WasPassed()) { endTime = ConvertMarkToTimestamp(ResolveTimestampAttribute::End, aOptions->mEnd.Value(), aRv); } else if (aOptions && aOptions->mStart.WasPassed() && aOptions->mDuration.WasPassed()) { const DOMHighResTimeStamp start = ConvertMarkToTimestamp( ResolveTimestampAttribute::Start, aOptions->mStart.Value(), aRv); 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) { DOMHighResTimeStamp startTime; if (aOptions && aOptions->mStart.WasPassed()) { startTime = ConvertMarkToTimestamp(ResolveTimestampAttribute::Start, aOptions->mStart.Value(), aRv); } 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); if (aRv.Failed()) { return 0; } startTime = end - duration; } else if (aStartMark) { startTime = ConvertMarkToTimestampWithString(*aStartMark, aRv); } else { startTime = 0; } return startTime; } already_AddRefed Performance::Measure(JSContext* aCx, const nsAString& aName, const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions, const Optional& aEndMark, ErrorResult& aRv) { if (!GetParentObject()) { aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } // Don't add the entry if the buffer is full. XXX should be removed by bug // 1159003. if (mUserEntries.Length() >= mResourceTimingBufferSize) { 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(); return nullptr; } if (!options->mStart.WasPassed() && !options->mEnd.WasPassed()) { aRv.ThrowTypeError(); return nullptr; } if (options->mStart.WasPassed() && options->mDuration.WasPassed() && options->mEnd.WasPassed()) { aRv.ThrowTypeError(); return nullptr; } } const DOMHighResTimeStamp endTime = ResolveEndTimeForMeasure(aEndMark, options, aRv); 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); 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( GetAsISupports(), aName, startTime, endTime, detail); InsertUserEntry(performanceMeasure); return performanceMeasure.forget(); } void Performance::ClearMeasures(const Optional& aName) { ClearUserEntries(aName, NS_LITERAL_STRING("measure")); } 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()).get(), NS_ConvertUTF16toUTF8(aEntry->GetName()).get(), aEntry->StartTime(), aEntry->Duration(), static_cast(PR_Now() / PR_USEC_PER_MSEC)); } void Performance::TimingNotification(PerformanceEntry* aEntry, const nsACString& aOwner, uint64_t aEpoch) { PerformanceEntryEventInit init; init.mBubbles = false; init.mCancelable = false; init.mName = aEntry->GetName(); init.mEntryType = aEntry->GetEntryType(); init.mStartTime = aEntry->StartTime(); init.mDuration = aEntry->Duration(); init.mEpoch = aEpoch; init.mOrigin = NS_ConvertUTF8toUTF16(aOwner.BeginReading()); RefPtr perfEntryEvent = PerformanceEntryEvent::Constructor(this, NS_LITERAL_STRING("performanceentry"), init); nsCOMPtr et = do_QueryInterface(GetOwner()); if (et) { bool dummy = false; et->DispatchEvent(perfEntryEvent, &dummy); } } void Performance::InsertUserEntry(PerformanceEntry* aEntry) { mUserEntries.InsertElementSorted(aEntry, PerformanceEntryComparator()); QueueEntry(aEntry); } void Performance::SetResourceTimingBufferSize(uint64_t aMaxSize) { mResourceTimingBufferSize = aMaxSize; } void Performance::InsertResourceEntry(PerformanceEntry* aEntry) { MOZ_ASSERT(aEntry); MOZ_ASSERT(mResourceEntries.Length() < mResourceTimingBufferSize); if (mResourceEntries.Length() >= mResourceTimingBufferSize) { return; } mResourceEntries.InsertElementSorted(aEntry, PerformanceEntryComparator()); if (mResourceEntries.Length() == mResourceTimingBufferSize) { // call onresourcetimingbufferfull DispatchBufferFullEvent(); } QueueEntry(aEntry); } 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, PerformanceObserver, Notify, ()); } void Performance::CancelNotificationObservers() { mPendingNotificationObserversTask = false; } class NotifyObserversTask final : public CancelableRunnable { public: explicit NotifyObserversTask(Performance* aPerformance) : mPerformance(aPerformance) { MOZ_ASSERT(mPerformance); } 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() { } RefPtr mPerformance; }; void Performance::RunNotificationObserversTask() { mPendingNotificationObserversTask = true; nsCOMPtr task = new NotifyObserversTask(this); nsresult rv; if (NS_IsMainThread()) { rv = NS_DispatchToCurrentThread(task); } else { rv = NS_DispatchToMainThread(task); } if (NS_WARN_IF(NS_FAILED(rv))) { mPendingNotificationObserversTask = false; } } void Performance::QueueEntry(PerformanceEntry* aEntry) { if (mObservers.IsEmpty()) { return; } nsTObserverArray interestedObservers; nsTObserverArray::ForwardIterator observerIt( mObservers); while (observerIt.HasMore()) { PerformanceObserver* observer = observerIt.GetNext(); if (observer->ObservesTypeOfEntry(aEntry)) { interestedObservers.AppendElement(observer); } } if (interestedObservers.IsEmpty()) { return; } NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(interestedObservers, PerformanceObserver, QueueEntry, (aEntry)); if (!mPendingNotificationObserversTask) { RunNotificationObserversTask(); } } /* static */ bool Performance::IsEnabled(JSContext* aCx, JSObject* aGlobal) { if (NS_IsMainThread()) { return Preferences::GetBool("dom.enable_user_timing", false); } WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); RefPtr runnable = new PrefEnabledRunnable(workerPrivate, NS_LITERAL_CSTRING("dom.enable_user_timing")); return runnable->Dispatch() && runnable->IsEnabled(); } /* static */ bool Performance::IsObserverEnabled(JSContext* aCx, JSObject* aGlobal) { if (NS_IsMainThread()) { return Preferences::GetBool("dom.enable_performance_observer", false); } WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate(); MOZ_ASSERT(workerPrivate); workerPrivate->AssertIsOnWorkerThread(); RefPtr runnable = new PrefEnabledRunnable(workerPrivate, NS_LITERAL_CSTRING("dom.enable_performance_observer")); return runnable->Dispatch() && runnable->IsEnabled(); } } // dom namespace } // mozilla namespace