gecko-dev/dom/performance/PerformanceMainThread.cpp
Michael Comella 312bff454e Bug 1772417 - align IsPerformanceTimingAttribute to user-timing spec. r=sefeng
IsPerformanceTimingAttribute can be called by both Window and WorkerGlobalScope
global objects so we move it to the Performance* superclass.

Differential Revision: https://phabricator.services.mozilla.com/D154819
2022-08-18 21:18:24 +00:00

592 lines
19 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 "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<nsIChannel> channel = do_QueryInterface(aChannel);
if (!channel) {
return;
}
nsCOMPtr<nsIURI> 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(nsISupports)
NS_INTERFACE_MAP_END_INHERITING(Performance)
PerformanceMainThread::PerformanceMainThread(nsPIDOMWindowInner* aWindow,
nsDOMNavigationTiming* aDOMTiming,
nsITimedChannel* aChannel,
bool aPrincipal)
: Performance(aWindow, aPrincipal),
mDOMTiming(aDOMTiming),
mChannel(aChannel),
mCrossOriginIsolated(aWindow->AsGlobal()->CrossOriginIsolated()) {
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<JSObject*> aObj) {
if (!mMozMemory) {
JS::Rooted<JSObject*> mozMemoryObj(aCx, JS_NewPlainObject(aCx));
JS::Rooted<JSObject*> 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> 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(
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<PerformanceTimingData>&& aData) {
AddRawEntry(std::move(aData), initiatorType, entryName);
}
void PerformanceMainThread::AddRawEntry(UniquePtr<PerformanceTimingData> aData,
const nsAString& aInitiatorType,
const nsAString& aEntryName) {
// The PerformanceResourceTiming object will use the PerformanceTimingData
// object to get all the required timings.
auto entry =
MakeRefPtr<PerformanceResourceTiming>(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<PerformanceMainThread>(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<PerformanceEventTiming> 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(), /* aIsSystemPrinciapl */ false,
CrossOriginIsolated());
}
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<nsPIDOMWindowInner> 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<double>(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() ||
StaticPrefs::privacy_resistFingerprinting()) {
return;
}
nsAutoString name;
GetURLSpecFromChannel(mChannel, name);
UniquePtr<PerformanceTimingData> timing(
new PerformanceTimingData(mChannel, nullptr, 0));
nsCOMPtr<nsIHttpChannel> 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<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel);
if (httpChannel) {
mDocEntry->UpdatePropertiesFromHttpChannel(httpChannel, mChannel);
}
}
void PerformanceMainThread::QueueNavigationTimingEntry() {
if (!mDocEntry) {
return;
}
UpdateNavigationTimingEntry();
QueueEntry(mDocEntry);
}
bool PerformanceMainThread::CrossOriginIsolated() const {
return mCrossOriginIsolated;
}
EventCounts* PerformanceMainThread::EventCounts() {
MOZ_ASSERT(StaticPrefs::dom_enable_event_timing());
return mEventCounts;
}
void PerformanceMainThread::GetEntries(
nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
// We return an empty list when 'privacy.resistFingerprinting' is on.
if (nsContentUtils::ShouldResistFingerprinting()) {
aRetval.Clear();
return;
}
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<RefPtr<PerformanceEntry>>& aRetval) {
// We return an empty list when 'privacy.resistFingerprinting' is on.
if (nsContentUtils::ShouldResistFingerprinting()) {
aRetval.Clear();
return;
}
RefPtr<nsAtom> 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<RefPtr<PerformanceEntry>>& aRetval) {
if (aEntryType.EqualsLiteral("event")) {
aRetval.AppendElements(mEventTimingEntries);
return;
}
return GetEntriesByType(aEntryType, aRetval);
}
void PerformanceMainThread::GetEntriesByName(
const nsAString& aName, const Optional<nsAString>& aEntryType,
nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
// We return an empty list when 'privacy.resistFingerprinting' is on.
if (nsContentUtils::ShouldResistFingerprinting()) {
aRetval.Clear();
return;
}
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