/* -*- 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/dom/PerformanceTimingBinding.h" #include "mozilla/Telemetry.h" #include "nsIDocShell.h" #include "nsIDocShellTreeItem.h" #include "nsIDocument.h" #include "nsITimedChannel.h" namespace mozilla { namespace dom { NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PerformanceTiming, mPerformance) NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(PerformanceTiming, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(PerformanceTiming, Release) /* 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 (!nsContentUtils::IsResourceTimingEnabled()) { 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 = NS_LITERAL_STRING("other"); } // 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); aEntryName = NS_ConvertUTF8toUTF16(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). 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, aPerformance->IsSystemPrincipal() ? aZeroTime : nsRFPService::ReduceTimePrecisionAsMSecs(aZeroTime, aPerformance->GetRandomTimelineSeed()))); // Non-null aHttpChannel implies that this PerformanceTiming object is being // used for subresources, which is irrelevant to this probe. if (!aHttpChannel && nsContentUtils::IsPerformanceTimingEnabled() && 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) , mReportCrossOriginRedirect(true) , mSecureConnection(false) , mTimingAllowed(true) , mInitialized(false) { mInitialized = !!aChannel; mZeroTime = aZeroTime; if (!nsContentUtils::IsPerformanceTimingEnabled() || nsContentUtils::ShouldResistFingerprinting()) { 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) { nsresult rv = uri->SchemeIs("https", &mSecureConnection); if (NS_FAILED(rv)) { mSecureConnection = false; } } 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->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); aChannel->GetNativeServerTiming(mServerTiming); // 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) { mTimingAllowed = CheckAllowedOrigin(aHttpChannel, aChannel); bool redirectsPassCheck = false; aChannel->GetAllRedirectsPassTimingAllowCheck(&redirectsPassCheck); mReportCrossOriginRedirect = mTimingAllowed && redirectsPassCheck; SetPropertiesFromHttpChannel(aHttpChannel); } } void PerformanceTimingData::SetPropertiesFromHttpChannel(nsIHttpChannel* aHttpChannel) { MOZ_ASSERT(aHttpChannel); nsAutoCString protocol; Unused << aHttpChannel->GetProtocolVersion(protocol); mNextHopProtocol = NS_ConvertUTF8toUTF16(protocol); Unused << aHttpChannel->GetEncodedBodySize(&mEncodedBodySize); Unused << aHttpChannel->GetTransferSize(&mTransferSize); Unused << aHttpChannel->GetDecodedBodySize(&mDecodedBodySize); if (mDecodedBodySize == 0) { mDecodedBodySize = mEncodedBodySize; } } PerformanceTiming::~PerformanceTiming() { } DOMHighResTimeStamp PerformanceTimingData::FetchStartHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!mFetchStart) { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { 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); } } } if (aPerformance->IsSystemPrincipal()) { return mFetchStart; } return nsRFPService::ReduceTimePrecisionAsMSecs(mFetchStart, aPerformance->GetRandomTimelineSeed()); } 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->GetLoadInfo(getter_AddRefs(loadInfo)); if (!loadInfo) { return false; } // TYPE_DOCUMENT loads have no loadingPrincipal. And that's OK, because we // never actually need to have a performance timing entry for TYPE_DOCUMENT // loads. if (loadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_DOCUMENT) { 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); } uint8_t PerformanceTimingData::GetRedirectCount() const { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { return 0; } if (!mAllRedirectsSameOrigin) { return 0; } return mRedirectCount; } bool PerformanceTimingData::ShouldReportCrossOriginRedirect() const { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { 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; } DOMHighResTimeStamp PerformanceTimingData::AsyncOpenHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting() || mAsyncOpen.IsNull()) { return mZeroTime; } DOMHighResTimeStamp rawValue = TimeStampToDOMHighRes(aPerformance, mAsyncOpen); if (aPerformance->IsSystemPrincipal()) { return rawValue; } return nsRFPService::ReduceTimePrecisionAsMSecs(rawValue, aPerformance->GetRandomTimelineSeed()); } DOMHighResTimeStamp PerformanceTimingData::WorkerStartHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting() || mWorkerStart.IsNull()) { return mZeroTime; } DOMHighResTimeStamp rawValue = TimeStampToDOMHighRes(aPerformance, mWorkerStart); if (aPerformance->IsSystemPrincipal()) { return rawValue; } return nsRFPService::ReduceTimePrecisionAsMSecs(rawValue, aPerformance->GetRandomTimelineSeed()); } /** * 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 (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { 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 (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { 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 (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { return mZeroTime; } return TimeStampToReducedDOMHighResOrFetchStart(aPerformance, mDomainLookupStart); } DOMTimeMilliSec PerformanceTiming::DomainLookupStart() { return static_cast(mTimingData->DomainLookupStartHighRes(mPerformance)); } DOMHighResTimeStamp PerformanceTimingData::DomainLookupEndHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { return mZeroTime; } // Bug 1155008 - nsHttpTransaction is racy. Return DomainLookupStart when null if (mDomainLookupEnd.IsNull()) { return DomainLookupStartHighRes(aPerformance); } DOMHighResTimeStamp rawValue = TimeStampToDOMHighRes(aPerformance, mDomainLookupEnd); if (aPerformance->IsSystemPrincipal()) { return rawValue; } return nsRFPService::ReduceTimePrecisionAsMSecs(rawValue, aPerformance->GetRandomTimelineSeed()); } DOMTimeMilliSec PerformanceTiming::DomainLookupEnd() { return static_cast(mTimingData->DomainLookupEndHighRes(mPerformance)); } DOMHighResTimeStamp PerformanceTimingData::ConnectStartHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { return mZeroTime; } if (mConnectStart.IsNull()) { return DomainLookupEndHighRes(aPerformance); } DOMHighResTimeStamp rawValue = TimeStampToDOMHighRes(aPerformance, mConnectStart); if (aPerformance->IsSystemPrincipal()) { return rawValue; } return nsRFPService::ReduceTimePrecisionAsMSecs(rawValue, aPerformance->GetRandomTimelineSeed()); } DOMTimeMilliSec PerformanceTiming::ConnectStart() { return static_cast(mTimingData->ConnectStartHighRes(mPerformance)); } DOMHighResTimeStamp PerformanceTimingData::SecureConnectionStartHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { return mZeroTime; } if (!mSecureConnection) { return 0; // We use 0 here, because mZeroTime is sometimes set to the navigation // start time. } if (mSecureConnectionStart.IsNull()) { return mZeroTime; } DOMHighResTimeStamp rawValue = TimeStampToDOMHighRes(aPerformance, mSecureConnectionStart); if (aPerformance->IsSystemPrincipal()) { return rawValue; } return nsRFPService::ReduceTimePrecisionAsMSecs(rawValue, aPerformance->GetRandomTimelineSeed()); } DOMTimeMilliSec PerformanceTiming::SecureConnectionStart() { return static_cast(mTimingData->SecureConnectionStartHighRes(mPerformance)); } DOMHighResTimeStamp PerformanceTimingData::ConnectEndHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { return mZeroTime; } // Bug 1155008 - nsHttpTransaction is racy. Return ConnectStart when null if (mConnectEnd.IsNull()) { return ConnectStartHighRes(aPerformance); } DOMHighResTimeStamp rawValue = TimeStampToDOMHighRes(aPerformance, mConnectEnd); if (aPerformance->IsSystemPrincipal()) { return rawValue; } return nsRFPService::ReduceTimePrecisionAsMSecs(rawValue, aPerformance->GetRandomTimelineSeed()); } DOMTimeMilliSec PerformanceTiming::ConnectEnd() { return static_cast(mTimingData->ConnectEndHighRes(mPerformance)); } DOMHighResTimeStamp PerformanceTimingData::RequestStartHighRes(Performance* aPerformance) { MOZ_ASSERT(aPerformance); if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { 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 (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { 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 (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || nsContentUtils::ShouldResistFingerprinting()) { 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); if (aPerformance->IsSystemPrincipal()) { return rawValue; } return nsRFPService::ReduceTimePrecisionAsMSecs(rawValue, aPerformance->GetRandomTimelineSeed()); } DOMTimeMilliSec PerformanceTiming::ResponseEnd() { return static_cast(mTimingData->ResponseEndHighRes(mPerformance)); } JSObject* PerformanceTiming::WrapObject(JSContext *cx, JS::Handle aGivenProto) { return PerformanceTimingBinding::Wrap(cx, this, aGivenProto); } bool PerformanceTiming::IsTopLevelContentDocument() const { nsCOMPtr document = mPerformance->GetDocumentIfCurrent(); if (!document) { return false; } nsCOMPtr docShell = document->GetDocShell(); if (!docShell) { return false; } nsCOMPtr rootItem; Unused << docShell->GetSameTypeRootTreeItem(getter_AddRefs(rootItem)); if (rootItem.get() != static_cast(docShell.get())) { return false; } return rootItem->ItemType() == nsIDocShellTreeItem::typeContent; } nsTArray> PerformanceTimingData::GetServerTiming() { if (!nsContentUtils::IsPerformanceTimingEnabled() || !IsInitialized() || !TimingAllowed() || nsContentUtils::ShouldResistFingerprinting()) { return nsTArray>(); } return nsTArray>(mServerTiming); } } // dom namespace } // mozilla namespace