gecko-dev/dom/performance/PerformanceTiming.cpp

676 строки
22 KiB
C++

/* -*- 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<nsIURI> 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<nsIURI> uri;
if (aHttpChannel) {
aHttpChannel->GetURI(getter_AddRefs(uri));
} else {
nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel);
if (httpChannel) {
httpChannel->GetURI(getter_AddRefs(uri));
}
}
if (uri) {
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);
// 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<int64_t>(mTimingData->FetchStartHighRes(mPerformance));
}
bool
PerformanceTimingData::CheckAllowedOrigin(nsIHttpChannel* aResourceChannel,
nsITimedChannel* aChannel)
{
if (!IsInitialized()) {
return false;
}
// Check that the current document passes the ckeck.
nsCOMPtr<nsILoadInfo> loadInfo;
aResourceChannel->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<nsIPrincipal> 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<int64_t>(mTimingData->RedirectStartHighRes(mPerformance));
}
return 0;
}
/**
* RedirectEndHighRes() is used by both the navigation timing and the resource
* timing. Since, navigation timing and resource timing check and interpret
* cross-domain redirects in a different manner, RedirectEndHighRes() will make
* no checks for cross-domain redirect. It's up to the consumers of this method
* (PerformanceTiming::RedirectEnd() and
* PerformanceResourceTiming::RedirectEnd() to make such verifications.
*
* @return a valid timing if the Performance Timing is enabled
*/
DOMHighResTimeStamp
PerformanceTimingData::RedirectEndHighRes(Performance* aPerformance)
{
MOZ_ASSERT(aPerformance);
if (!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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(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<int64_t>(mTimingData->ResponseEndHighRes(mPerformance));
}
JSObject*
PerformanceTiming::WrapObject(JSContext *cx, JS::Handle<JSObject*> aGivenProto)
{
return PerformanceTimingBinding::Wrap(cx, this, aGivenProto);
}
bool
PerformanceTiming::IsTopLevelContentDocument() const
{
nsCOMPtr<nsIDocument> document = mPerformance->GetDocumentIfCurrent();
if (!document) {
return false;
}
nsCOMPtr<nsIDocShell> docShell = document->GetDocShell();
if (!docShell) {
return false;
}
nsCOMPtr<nsIDocShellTreeItem> rootItem;
Unused << docShell->GetSameTypeRootTreeItem(getter_AddRefs(rootItem));
if (rootItem.get() != static_cast<nsIDocShellTreeItem*>(docShell.get())) {
return false;
}
return rootItem->ItemType() == nsIDocShellTreeItem::typeContent;
}
} // dom namespace
} // mozilla namespace