/* -*- Mode: C++; tab-width: 2; 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 "nsPerformance.h" #include "nsCOMPtr.h" #include "nsIHttpChannel.h" #include "nsITimedChannel.h" #include "nsDOMNavigationTiming.h" #include "nsContentUtils.h" #include "nsIScriptSecurityManager.h" #include "nsIDOMWindow.h" #include "nsIURI.h" #include "PerformanceEntry.h" #include "PerformanceResourceTiming.h" #include "mozilla/dom/PerformanceBinding.h" #include "mozilla/dom/PerformanceTimingBinding.h" #include "mozilla/dom/PerformanceNavigationBinding.h" #include "mozilla/TimeStamp.h" #include "nsThreadUtils.h" #include "nsILoadInfo.h" using namespace mozilla; NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsPerformanceTiming, mPerformance) NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsPerformanceTiming, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsPerformanceTiming, Release) nsPerformanceTiming::nsPerformanceTiming(nsPerformance* aPerformance, nsITimedChannel* aChannel, nsIHttpChannel* aHttpChannel, DOMHighResTimeStamp aZeroTime) : mPerformance(aPerformance), mFetchStart(0.0), mZeroTime(aZeroTime), mRedirectCount(0), mTimingAllowed(true), mAllRedirectsSameOrigin(true), mInitialized(!!aChannel), mReportCrossOriginRedirect(true) { MOZ_ASSERT(aPerformance, "Parent performance object should be provided"); if (!nsContentUtils::IsPerformanceTimingEnabled()) { mZeroTime = 0; } // The aHttpChannel argument is null if this nsPerformanceTiming object // is being used for the navigation timing (document) and has a non-null // value for the resource timing (any resources within the page). if (aHttpChannel) { mTimingAllowed = CheckAllowedOrigin(aHttpChannel, aChannel); bool redirectsPassCheck = false; aChannel->GetAllRedirectsPassTimingAllowCheck(&redirectsPassCheck); mReportCrossOriginRedirect = mTimingAllowed && redirectsPassCheck; } InitializeTimingInfo(aChannel); } // Copy the timing info from the channel so we don't need to keep the channel // alive just to get the timestamps. void nsPerformanceTiming::InitializeTimingInfo(nsITimedChannel* aChannel) { if (aChannel) { aChannel->GetAsyncOpen(&mAsyncOpen); aChannel->GetAllRedirectsSameOrigin(&mAllRedirectsSameOrigin); aChannel->GetRedirectCount(&mRedirectCount); aChannel->GetRedirectStart(&mRedirectStart); aChannel->GetRedirectEnd(&mRedirectEnd); aChannel->GetDomainLookupStart(&mDomainLookupStart); aChannel->GetDomainLookupEnd(&mDomainLookupEnd); aChannel->GetConnectStart(&mConnectStart); aChannel->GetConnectEnd(&mConnectEnd); aChannel->GetRequestStart(&mRequestStart); aChannel->GetResponseStart(&mResponseStart); aChannel->GetCacheReadStart(&mCacheReadStart); aChannel->GetResponseEnd(&mResponseEnd); aChannel->GetCacheReadEnd(&mCacheReadEnd); } } nsPerformanceTiming::~nsPerformanceTiming() { } DOMHighResTimeStamp nsPerformanceTiming::FetchStartHighRes() { if (!mFetchStart) { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } MOZ_ASSERT(!mAsyncOpen.IsNull(), "The fetch start time stamp should always be " "valid if the performance timing is enabled"); mFetchStart = (!mAsyncOpen.IsNull()) ? TimeStampToDOMHighRes(mAsyncOpen) : 0.0; } return mFetchStart; } DOMTimeMilliSec nsPerformanceTiming::FetchStart() { return static_cast(FetchStartHighRes()); } bool nsPerformanceTiming::CheckAllowedOrigin(nsIHttpChannel* aResourceChannel, nsITimedChannel* aChannel) { if (!IsInitialized()) { return false; } // Check that the current document passes the ckeck. nsCOMPtr loadInfo; aResourceChannel->GetLoadInfo(getter_AddRefs(loadInfo)); if (!loadInfo) { return false; } nsCOMPtr principal = loadInfo->LoadingPrincipal(); // 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); } bool nsPerformanceTiming::TimingAllowed() const { return mTimingAllowed; } uint16_t nsPerformanceTiming::GetRedirectCount() const { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return 0; } if (!mAllRedirectsSameOrigin) { return 0; } return mRedirectCount; } bool nsPerformanceTiming::ShouldReportCrossOriginRedirect() const { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { 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 (mRedirectCount != 0) && mReportCrossOriginRedirect; } /** * 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 (nsPerformanceTiming::RedirectStart() * and PerformanceResourceTiming::RedirectStart() to make such verifications. * * @return a valid timing if the Performance Timing is enabled */ DOMHighResTimeStamp nsPerformanceTiming::RedirectStartHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } return TimeStampToDOMHighResOrFetchStart(mRedirectStart); } DOMTimeMilliSec nsPerformanceTiming::RedirectStart() { if (!IsInitialized()) { return mZeroTime; } // We have to check if all the redirect URIs had the same origin (since there // is no check in RedirectStartHighRes()) if (mAllRedirectsSameOrigin) { return static_cast(RedirectStartHighRes()); } 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 * (nsPerformanceTiming::RedirectEnd() and * PerformanceResourceTiming::RedirectEnd() to make such verifications. * * @return a valid timing if the Performance Timing is enabled */ DOMHighResTimeStamp nsPerformanceTiming::RedirectEndHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } return TimeStampToDOMHighResOrFetchStart(mRedirectEnd); } DOMTimeMilliSec nsPerformanceTiming::RedirectEnd() { if (!IsInitialized()) { return mZeroTime; } // We have to check if all the redirect URIs had the same origin (since there // is no check in RedirectEndHighRes()) if (mAllRedirectsSameOrigin) { return static_cast(RedirectEndHighRes()); } return 0; } DOMHighResTimeStamp nsPerformanceTiming::DomainLookupStartHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } return TimeStampToDOMHighResOrFetchStart(mDomainLookupStart); } DOMTimeMilliSec nsPerformanceTiming::DomainLookupStart() { return static_cast(DomainLookupStartHighRes()); } DOMHighResTimeStamp nsPerformanceTiming::DomainLookupEndHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } return TimeStampToDOMHighResOrFetchStart(mDomainLookupEnd); } DOMTimeMilliSec nsPerformanceTiming::DomainLookupEnd() { return static_cast(DomainLookupEndHighRes()); } DOMHighResTimeStamp nsPerformanceTiming::ConnectStartHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } return TimeStampToDOMHighResOrFetchStart(mConnectStart); } DOMTimeMilliSec nsPerformanceTiming::ConnectStart() { return static_cast(ConnectStartHighRes()); } DOMHighResTimeStamp nsPerformanceTiming::ConnectEndHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } return TimeStampToDOMHighResOrFetchStart(mConnectEnd); } DOMTimeMilliSec nsPerformanceTiming::ConnectEnd() { return static_cast(ConnectEndHighRes()); } DOMHighResTimeStamp nsPerformanceTiming::RequestStartHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } return TimeStampToDOMHighResOrFetchStart(mRequestStart); } DOMTimeMilliSec nsPerformanceTiming::RequestStart() { return static_cast(RequestStartHighRes()); } DOMHighResTimeStamp nsPerformanceTiming::ResponseStartHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } if (mResponseStart.IsNull() || (!mCacheReadStart.IsNull() && mCacheReadStart < mResponseStart)) { mResponseStart = mCacheReadStart; } return TimeStampToDOMHighResOrFetchStart(mResponseStart); } DOMTimeMilliSec nsPerformanceTiming::ResponseStart() { return static_cast(ResponseStartHighRes()); } DOMHighResTimeStamp nsPerformanceTiming::ResponseEndHighRes() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized()) { return mZeroTime; } if (mResponseEnd.IsNull() || (!mCacheReadEnd.IsNull() && mCacheReadEnd < mResponseEnd)) { mResponseEnd = mCacheReadEnd; } return TimeStampToDOMHighResOrFetchStart(mResponseEnd); } DOMTimeMilliSec nsPerformanceTiming::ResponseEnd() { return static_cast(ResponseEndHighRes()); } bool nsPerformanceTiming::IsInitialized() const { return mInitialized; } JSObject* nsPerformanceTiming::WrapObject(JSContext *cx) { return dom::PerformanceTimingBinding::Wrap(cx, this); } NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(nsPerformanceNavigation, mPerformance) NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsPerformanceNavigation, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsPerformanceNavigation, Release) nsPerformanceNavigation::nsPerformanceNavigation(nsPerformance* aPerformance) : mPerformance(aPerformance) { MOZ_ASSERT(aPerformance, "Parent performance object should be provided"); } nsPerformanceNavigation::~nsPerformanceNavigation() { } JSObject* nsPerformanceNavigation::WrapObject(JSContext *cx) { return dom::PerformanceNavigationBinding::Wrap(cx, this); } NS_IMPL_CYCLE_COLLECTION_INHERITED(nsPerformance, DOMEventTargetHelper, mWindow, mTiming, mNavigation, mEntries, mParentPerformance) NS_IMPL_ADDREF_INHERITED(nsPerformance, DOMEventTargetHelper) NS_IMPL_RELEASE_INHERITED(nsPerformance, DOMEventTargetHelper) nsPerformance::nsPerformance(nsPIDOMWindow* aWindow, nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel, nsPerformance* aParentPerformance) : DOMEventTargetHelper(aWindow), mWindow(aWindow), mDOMTiming(aDOMTiming), mChannel(aChannel), mParentPerformance(aParentPerformance), mPrimaryBufferSize(kDefaultBufferSize) { MOZ_ASSERT(aWindow, "Parent window object should be provided"); } nsPerformance::~nsPerformance() { } // QueryInterface implementation for nsPerformance NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsPerformance) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper) nsPerformanceTiming* nsPerformance::Timing() { if (!mTiming) { // For navigation timing, the third argument (an nsIHtttpChannel) 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 nsPerformanceTiming(this, mChannel, nullptr, mDOMTiming->GetNavigationStart()); } return mTiming; } void nsPerformance::DispatchBufferFullEvent() { nsCOMPtr event; nsresult rv = NS_NewDOMEvent(getter_AddRefs(event), this, nullptr, nullptr); if (NS_SUCCEEDED(rv)) { // it bubbles, and it isn't cancelable rv = event->InitEvent(NS_LITERAL_STRING("resourcetimingbufferfull"), true, false); if (NS_SUCCEEDED(rv)) { event->SetTrusted(true); DispatchDOMEvent(nullptr, event, nullptr, nullptr); } } } nsPerformanceNavigation* nsPerformance::Navigation() { if (!mNavigation) { mNavigation = new nsPerformanceNavigation(this); } return mNavigation; } DOMHighResTimeStamp nsPerformance::Now() { return GetDOMTiming()->TimeStampToDOMHighRes(mozilla::TimeStamp::Now()); } JSObject* nsPerformance::WrapObject(JSContext *cx) { return dom::PerformanceBinding::Wrap(cx, this); } void nsPerformance::GetEntries(nsTArray >& retval) { MOZ_ASSERT(NS_IsMainThread()); retval = mEntries; } void nsPerformance::GetEntriesByType(const nsAString& entryType, nsTArray >& retval) { MOZ_ASSERT(NS_IsMainThread()); retval.Clear(); uint32_t count = mEntries.Length(); for (uint32_t i = 0 ; i < count; i++) { if (mEntries[i]->GetEntryType().Equals(entryType)) { retval.AppendElement(mEntries[i]); } } } void nsPerformance::GetEntriesByName(const nsAString& name, const mozilla::dom::Optional& entryType, nsTArray >& retval) { MOZ_ASSERT(NS_IsMainThread()); retval.Clear(); uint32_t count = mEntries.Length(); for (uint32_t i = 0 ; i < count; i++) { if (mEntries[i]->GetName().Equals(name) && (!entryType.WasPassed() || mEntries[i]->GetEntryType().Equals(entryType.Value()))) { retval.AppendElement(mEntries[i]); } } } void nsPerformance::ClearResourceTimings() { MOZ_ASSERT(NS_IsMainThread()); mEntries.Clear(); } void nsPerformance::SetResourceTimingBufferSize(uint64_t maxSize) { MOZ_ASSERT(NS_IsMainThread()); mPrimaryBufferSize = maxSize; } /** * 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 nsPerformance::AddEntry(nsIHttpChannel* channel, nsITimedChannel* timedChannel) { MOZ_ASSERT(NS_IsMainThread()); // Check if resource timing is prefed off. if (!nsContentUtils::IsResourceTimingEnabled()) { return; } // Don't add the entry if the buffer is full if (mEntries.Length() >= mPrimaryBufferSize) { return; } if (channel && timedChannel) { nsAutoCString name; nsAutoString initiatorType; nsCOMPtr originalURI; timedChannel->GetInitiatorType(initiatorType); // 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." channel->GetOriginalURI(getter_AddRefs(originalURI)); originalURI->GetSpec(name); NS_ConvertUTF8toUTF16 entryName(name); // 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). nsRefPtr performanceTiming = new nsPerformanceTiming(this, timedChannel, channel, 0); // The PerformanceResourceTiming object will use the nsPerformanceTiming // object to get all the required timings. nsRefPtr performanceEntry = new dom::PerformanceResourceTiming(performanceTiming, this); performanceEntry->SetName(entryName); performanceEntry->SetEntryType(NS_LITERAL_STRING("resource")); // If the initiator type had no valid value, then set it to the default // ("other") value. if (initiatorType.IsEmpty()) { initiatorType = NS_LITERAL_STRING("other"); } performanceEntry->SetInitiatorType(initiatorType); mEntries.InsertElementSorted(performanceEntry, PerformanceEntryComparator()); if (mEntries.Length() >= mPrimaryBufferSize) { // call onresourcetimingbufferfull DispatchBufferFullEvent(); } } } bool nsPerformance::PerformanceEntryComparator::Equals( const PerformanceEntry* aElem1, const PerformanceEntry* aElem2) const { NS_ABORT_IF_FALSE(aElem1 && aElem2, "Trying to compare null performance entries"); return aElem1->StartTime() == aElem2->StartTime(); } bool nsPerformance::PerformanceEntryComparator::LessThan( const PerformanceEntry* aElem1, const PerformanceEntry* aElem2) const { NS_ABORT_IF_FALSE(aElem1 && aElem2, "Trying to compare null performance entries"); return aElem1->StartTime() < aElem2->StartTime(); }