Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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 "Performance.h"
#include <sstream>
#if defined(XP_LINUX)
# include <fcntl.h>
# include <sys/mman.h>
#endif
#include "ETWTools.h"
#include "GeckoProfiler.h"
#include "nsRFPService.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/BasePrincipal.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/MessagePortBinding.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/IntegerPrintfMacros.h"
#include "mozilla/Perfetto.h"
#include "mozilla/Preferences.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/WorkerPrivate.h"
#include "mozilla/dom/WorkerRunnable.h"
#include "mozilla/dom/WorkerScope.h"
#define PERFLOG(msg, ...) printf_stderr(msg, ##__VA_ARGS__)
namespace mozilla::dom {
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,
mSecondaryResourceEntries, mObservers);
NS_IMPL_ADDREF_INHERITED(Performance, DOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(Performance, DOMEventTargetHelper)
// Used to dump performance timing information to a local file.
// Only defined when the env variable MOZ_USE_PERFORMANCE_MARKER_FILE
// is set and initialized by MaybeOpenMarkerFile().
static FILE* sMarkerFile = nullptr;
/* static */
already_AddRefed<Performance> Performance::CreateForMainThread(
nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
nsDOMNavigationTiming* aDOMTiming, nsITimedChannel* aChannel) {
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(aWindow->AsGlobal());
RefPtr<Performance> performance =
new PerformanceMainThread(aWindow, aDOMTiming, aChannel);
return performance.forget();
}
/* static */
already_AddRefed<Performance> Performance::CreateForWorker(
WorkerGlobalScope* aGlobalScope) {
MOZ_ASSERT(aGlobalScope);
// aWorkerPrivate->AssertIsOnWorkerThread();
RefPtr<Performance> performance = new PerformanceWorker(aGlobalScope);
return performance.forget();
}
/* static */
already_AddRefed<Performance> Performance::Get(JSContext* aCx,
nsIGlobalObject* aGlobal) {
RefPtr<Performance> performance;
if (NS_IsMainThread()) {
nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal);
if (!window) {
return nullptr;
}
performance = window->GetPerformance();
return performance.forget();
}
const WorkerPrivate* workerPrivate = GetWorkerPrivateFromContext(aCx);
if (!workerPrivate) {
return nullptr;
}
WorkerGlobalScope* scope = workerPrivate->GlobalScope();
MOZ_ASSERT(scope);
performance = scope->GetPerformance();
return performance.forget();
}
Performance::Performance(nsIGlobalObject* aGlobal)
: DOMEventTargetHelper(aGlobal),
mResourceTimingBufferSize(kDefaultResourceTimingBufferSize),
mPendingNotificationObserversTask(false),
mPendingResourceTimingBufferFullEvent(false),
mRTPCallerType(aGlobal->GetRTPCallerType()),
mCrossOriginIsolated(aGlobal->CrossOriginIsolated()),
mShouldResistFingerprinting(aGlobal->ShouldResistFingerprinting(
RFPTarget::ReduceTimerPrecision)) {}
Performance::~Performance() = default;
DOMHighResTimeStamp Performance::TimeStampToDOMHighResForRendering(
TimeStamp aTimeStamp) const {
DOMHighResTimeStamp stamp = GetDOMTiming()->TimeStampToDOMHighRes(aTimeStamp);
// 0 is an inappropriate mixin for this this area; however CSS Animations
// needs to have it's Time Reduction Logic refactored, so it's currently
// only clamping for RFP mode. RFP mode gives a much lower time precision,
// so we accept the security leak here for now.
return nsRFPService::ReduceTimePrecisionAsMSecsRFPOnly(stamp, 0,
mRTPCallerType);
}
DOMHighResTimeStamp Performance::Now() {
DOMHighResTimeStamp rawTime = NowUnclamped();
// XXX: Removing this caused functions in pkcs11f.h to fail.
// Bug 1628021 investigates the root cause - it involves initializing
// the RNG service (part of GetRandomTimelineSeed()) off-main-thread
// but the underlying cause hasn't been identified yet.
if (mRTPCallerType == RTPCallerType::SystemPrincipal) {
return rawTime;
}
return nsRFPService::ReduceTimePrecisionAsMSecs(
rawTime, GetRandomTimelineSeed(), mRTPCallerType);
}
DOMHighResTimeStamp Performance::NowUnclamped() const {
TimeDuration duration = TimeStamp::Now() - CreationTimeStamp();
return duration.ToMilliseconds();
}
DOMHighResTimeStamp Performance::TimeOrigin() {
if (!mPerformanceService) {
mPerformanceService = PerformanceService::GetOrCreate();
}
MOZ_ASSERT(mPerformanceService);
DOMHighResTimeStamp rawTimeOrigin =
mPerformanceService->TimeOrigin(CreationTimeStamp());
// Time Origin is an absolute timestamp, so we supply a 0 context mix-in
return nsRFPService::ReduceTimePrecisionAsMSecs(rawTimeOrigin, 0,
mRTPCallerType);
}
JSObject* Performance::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return Performance_Binding::Wrap(aCx, this, aGivenProto);
}
void Performance::GetEntries(nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
aRetval = mResourceEntries.Clone();
aRetval.AppendElements(mUserEntries);
aRetval.Sort(PerformanceEntryComparator());
}
void Performance::GetEntriesByType(
const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
if (aEntryType.EqualsLiteral("resource")) {
aRetval = mResourceEntries.Clone();
return;
}
aRetval.Clear();
if (aEntryType.EqualsLiteral("mark") || aEntryType.EqualsLiteral("measure")) {
RefPtr<nsAtom> entryType = NS_Atomize(aEntryType);
for (PerformanceEntry* entry : mUserEntries) {
if (entry->GetEntryType() == entryType) {
aRetval.AppendElement(entry);
}
}
}
}
void Performance::GetEntriesByName(
const nsAString& aName, const Optional<nsAString>& aEntryType,
nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
aRetval.Clear();
RefPtr<nsAtom> name = NS_Atomize(aName);
RefPtr<nsAtom> entryType =
aEntryType.WasPassed() ? NS_Atomize(aEntryType.Value()) : nullptr;
if (entryType) {
if (entryType == nsGkAtoms::mark || entryType == nsGkAtoms::measure) {
for (PerformanceEntry* entry : mUserEntries) {
if (entry->GetName() == name && entry->GetEntryType() == entryType) {
aRetval.AppendElement(entry);
}
}
return;
}
if (entryType == nsGkAtoms::resource) {
for (PerformanceEntry* entry : mResourceEntries) {
MOZ_ASSERT(entry->GetEntryType() == entryType);
if (entry->GetName() == name) {
aRetval.AppendElement(entry);
}
}
return;
}
// Invalid entryType
return;
}
nsTArray<PerformanceEntry*> qualifiedResourceEntries;
nsTArray<PerformanceEntry*> qualifiedUserEntries;
// ::Measure expects that results from this function are already
// passed through ReduceTimePrecision. mResourceEntries and mUserEntries
// are, so the invariant holds.
for (PerformanceEntry* entry : mResourceEntries) {
if (entry->GetName() == name) {
qualifiedResourceEntries.AppendElement(entry);
}
}
for (PerformanceEntry* entry : mUserEntries) {
if (entry->GetName() == name) {
qualifiedUserEntries.AppendElement(entry);
}
}
size_t resourceEntriesIdx = 0, userEntriesIdx = 0;
aRetval.SetCapacity(qualifiedResourceEntries.Length() +
qualifiedUserEntries.Length());
PerformanceEntryComparator comparator;
while (resourceEntriesIdx < qualifiedResourceEntries.Length() &&
userEntriesIdx < qualifiedUserEntries.Length()) {
if (comparator.LessThan(qualifiedResourceEntries[resourceEntriesIdx],
qualifiedUserEntries[userEntriesIdx])) {
aRetval.AppendElement(qualifiedResourceEntries[resourceEntriesIdx]);
++resourceEntriesIdx;
} else {
aRetval.AppendElement(qualifiedUserEntries[userEntriesIdx]);
++userEntriesIdx;
}
}
while (resourceEntriesIdx < qualifiedResourceEntries.Length()) {
aRetval.AppendElement(qualifiedResourceEntries[resourceEntriesIdx]);
++resourceEntriesIdx;
}
while (userEntriesIdx < qualifiedUserEntries.Length()) {
aRetval.AppendElement(qualifiedUserEntries[userEntriesIdx]);
++userEntriesIdx;
}
}
void Performance::GetEntriesByTypeForObserver(
const nsAString& aEntryType, nsTArray<RefPtr<PerformanceEntry>>& aRetval) {
GetEntriesByType(aEntryType, aRetval);
}
void Performance::ClearUserEntries(const Optional<nsAString>& aEntryName,
const nsAString& aEntryType) {
MOZ_ASSERT(!aEntryType.IsEmpty());
RefPtr<nsAtom> name =
aEntryName.WasPassed() ? NS_Atomize(aEntryName.Value()) : nullptr;
RefPtr<nsAtom> entryType = NS_Atomize(aEntryType);
mUserEntries.RemoveElementsBy([name, entryType](const auto& entry) {
return (!name || entry->GetName() == name) &&
(entry->GetEntryType() == entryType);
});
}
void Performance::ClearResourceTimings() { mResourceEntries.Clear(); }
struct UserTimingMarker : public BaseMarkerType<UserTimingMarker> {
static constexpr const char* Name = "UserTiming";
static constexpr const char* Description =
"UserTimingMeasure is created using the DOM API performance.measure().";
using MS = MarkerSchema;
static constexpr MS::PayloadField PayloadFields[] = {
{"name", MS::InputType::String, "User Marker Name", MS::Format::String,
MS::PayloadFlags::Searchable},
{"entryType", MS::InputType::Boolean, "Entry Type"},
{"startMark", MS::InputType::String, "Start Mark"},
{"endMark", MS::InputType::String, "End Mark"}};
static constexpr MS::Location Locations[] = {MS::Location::MarkerChart,
MS::Location::MarkerTable};
static constexpr const char* AllLabels = "{marker.data.name}";
static constexpr MS::ETWMarkerGroup Group = MS::ETWMarkerGroup::UserMarkers;
static void StreamJSONMarkerData(
baseprofiler::SpliceableJSONWriter& aWriter,
const ProfilerString16View& aName, bool aIsMeasure,
const Maybe<ProfilerString16View>& aStartMark,
const Maybe<ProfilerString16View>& aEndMark) {
StreamJSONMarkerDataImpl(
aWriter, aName,
aIsMeasure ? MakeStringSpan("measure") : MakeStringSpan("mark"),
aStartMark, aEndMark);
}
};
already_AddRefed<PerformanceMark> Performance::Mark(
JSContext* aCx, const nsAString& aName,
const PerformanceMarkOptions& aMarkOptions, ErrorResult& aRv) {
nsCOMPtr<nsIGlobalObject> parent = GetParentObject();
if (!parent || parent->IsDying() || !parent->HasJSGlobal()) {
aRv.ThrowInvalidStateError("Global object is unavailable");
return nullptr;
}
GlobalObject global(aCx, parent->GetGlobalJSObject());
if (global.Failed()) {
aRv.ThrowInvalidStateError("Global object is unavailable");
return nullptr;
}
RefPtr<PerformanceMark> performanceMark =
PerformanceMark::Constructor(global, aName, aMarkOptions, aRv);
if (aRv.Failed()) {
return nullptr;
}
InsertUserEntry(performanceMark);
if (profiler_thread_is_being_profiled_for_markers()) {
Maybe<uint64_t> innerWindowId;
if (nsGlobalWindowInner* owner = GetOwnerWindow()) {
innerWindowId = Some(owner->WindowID());
}
TimeStamp startTimeStamp =
CreationTimeStamp() +
TimeDuration::FromMilliseconds(performanceMark->UnclampedStartTime());
profiler_add_marker("UserTiming", geckoprofiler::category::DOM,
MarkerOptions(MarkerTiming::InstantAt(startTimeStamp),
MarkerInnerWindowId(innerWindowId)),
UserTimingMarker{}, aName, /* aIsMeasure */ false,
Nothing{}, Nothing{});
}
return performanceMark.forget();
}
void Performance::ClearMarks(const Optional<nsAString>& aName) {
ClearUserEntries(aName, u"mark"_ns);
}
// 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, bool aReturnUnclamped) {
if (IsPerformanceTimingAttribute(aName)) {
return ConvertNameToTimestamp(aName, aRv);
}
RefPtr<nsAtom> name = NS_Atomize(aName);
// Just loop over the user entries
for (const PerformanceEntry* entry : Reversed(mUserEntries)) {
if (entry->GetName() == name && entry->GetEntryType() == nsGkAtoms::mark) {
if (aReturnUnclamped) {
return entry->UnclampedStartTime();
}
return entry->StartTime();
}
}
nsPrintfCString errorMsg("Given mark name, %s, is unknown",
NS_ConvertUTF16toUTF8(aName).get());
aRv.ThrowSyntaxError(errorMsg);
return 0;
}
DOMHighResTimeStamp Performance::ConvertMarkToTimestampWithDOMHighResTimeStamp(
const ResolveTimestampAttribute aAttribute,
const DOMHighResTimeStamp aTimestamp, ErrorResult& aRv) {
if (aTimestamp < 0) {
nsAutoCString attributeName;
switch (aAttribute) {
case ResolveTimestampAttribute::Start:
attributeName = "start";
break;
case ResolveTimestampAttribute::End:
attributeName = "end";
break;
case ResolveTimestampAttribute::Duration:
attributeName = "duration";
break;
}
nsPrintfCString errorMsg("Given attribute %s cannot be negative",
attributeName.get());
aRv.ThrowTypeError(errorMsg);
}
return aTimestamp;
}
DOMHighResTimeStamp Performance::ConvertMarkToTimestamp(
const ResolveTimestampAttribute aAttribute,
const OwningStringOrDouble& aMarkNameOrTimestamp, ErrorResult& aRv,
bool aReturnUnclamped) {
if (aMarkNameOrTimestamp.IsString()) {
return ConvertMarkToTimestampWithString(aMarkNameOrTimestamp.GetAsString(),
aRv, aReturnUnclamped);
}
return ConvertMarkToTimestampWithDOMHighResTimeStamp(
aAttribute, aMarkNameOrTimestamp.GetAsDouble(), aRv);
}
DOMHighResTimeStamp Performance::ConvertNameToTimestamp(const nsAString& aName,
ErrorResult& aRv) {
if (!IsGlobalObjectWindow()) {
nsPrintfCString errorMsg(
"Cannot get PerformanceTiming attribute values for non-Window global "
"object. Given: %s",
NS_ConvertUTF16toUTF8(aName).get());
aRv.ThrowTypeError(errorMsg);
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(u"navigationStart"_ns);
const DOMHighResTimeStamp endTime = GetPerformanceTimingFromString(aName);
MOZ_ASSERT(endTime >= 0);
if (endTime == 0) {
nsPrintfCString errorMsg(
"Given PerformanceTiming attribute, %s, isn't available yet",
NS_ConvertUTF16toUTF8(aName).get());
aRv.ThrowInvalidAccessError(errorMsg);
return 0;
}
return endTime - startTime;
}
DOMHighResTimeStamp Performance::ResolveEndTimeForMeasure(
const Optional<nsAString>& aEndMark,
const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv,
bool aReturnUnclamped) {
DOMHighResTimeStamp endTime;
if (aEndMark.WasPassed()) {
endTime = ConvertMarkToTimestampWithString(aEndMark.Value(), aRv,
aReturnUnclamped);
} else if (aOptions && aOptions->mEnd.WasPassed()) {
endTime =
ConvertMarkToTimestamp(ResolveTimestampAttribute::End,
aOptions->mEnd.Value(), aRv, aReturnUnclamped);
} else if (aOptions && aOptions->mStart.WasPassed() &&
aOptions->mDuration.WasPassed()) {
const DOMHighResTimeStamp start =
ConvertMarkToTimestamp(ResolveTimestampAttribute::Start,
aOptions->mStart.Value(), aRv, aReturnUnclamped);
if (aRv.Failed()) {
return 0;
}
const DOMHighResTimeStamp duration =
ConvertMarkToTimestampWithDOMHighResTimeStamp(
ResolveTimestampAttribute::Duration, aOptions->mDuration.Value(),
aRv);
if (aRv.Failed()) {
return 0;
}
endTime = start + duration;
} else if (aReturnUnclamped) {
MOZ_DIAGNOSTIC_ASSERT(sMarkerFile ||
profiler_thread_is_being_profiled_for_markers());
endTime = NowUnclamped();
} else {
endTime = Now();
}
return endTime;
}
DOMHighResTimeStamp Performance::ResolveStartTimeForMeasure(
const Maybe<const nsAString&>& aStartMark,
const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv,
bool aReturnUnclamped) {
DOMHighResTimeStamp startTime;
if (aOptions && aOptions->mStart.WasPassed()) {
startTime =
ConvertMarkToTimestamp(ResolveTimestampAttribute::Start,
aOptions->mStart.Value(), aRv, aReturnUnclamped);
} 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, aReturnUnclamped);
if (aRv.Failed()) {
return 0;
}
startTime = end - duration;
} else if (aStartMark) {
startTime =
ConvertMarkToTimestampWithString(*aStartMark, aRv, aReturnUnclamped);
} else {
startTime = 0;
}
return startTime;
}
static std::string GetMarkerFilename() {
std::stringstream s;
if (char* markerDir = getenv("MOZ_PERFORMANCE_MARKER_DIR")) {
s << markerDir << "/";
}
#ifdef XP_WIN
s << "marker-" << GetCurrentProcessId() << ".txt";
#else
s << "marker-" << getpid() << ".txt";
#endif
return s.str();
}
std::pair<TimeStamp, TimeStamp> Performance::GetTimeStampsForMarker(
const Maybe<const nsAString&>& aStartMark,
const Optional<nsAString>& aEndMark,
const Maybe<const PerformanceMeasureOptions&>& aOptions, ErrorResult& aRv) {
const DOMHighResTimeStamp unclampedStartTime = ResolveStartTimeForMeasure(
aStartMark, aOptions, aRv, /* aReturnUnclamped */ true);
const DOMHighResTimeStamp unclampedEndTime =
ResolveEndTimeForMeasure(aEndMark, aOptions, aRv, /* aReturnUnclamped */
true);
TimeStamp startTimeStamp =
CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedStartTime);
TimeStamp endTimeStamp =
CreationTimeStamp() + TimeDuration::FromMilliseconds(unclampedEndTime);
return std::make_pair(startTimeStamp, endTimeStamp);
}
// Try to open the marker file for writing performance markers.
// If successful, returns true and sMarkerFile will be defined
// to the file handle. Otherwise, return false and sMarkerFile
// is NULL.
static bool MaybeOpenMarkerFile() {
if (!getenv("MOZ_USE_PERFORMANCE_MARKER_FILE")) {
return false;
}
// Check if it's already open.
if (sMarkerFile) {
return true;
}
#ifdef XP_LINUX
// We treat marker files similar to Jitdump files (see PerfSpewer.cpp) and
// mmap them if needed.
int fd = open(GetMarkerFilename().c_str(), O_CREAT | O_TRUNC | O_RDWR, 0666);
sMarkerFile = fdopen(fd, "w+");
if (!sMarkerFile) {
return false;
}
// On Linux and Android, we need to mmap the file so that the path makes it
// into the perf.data file or into samply.
// On non-Android, make the mapping executable, otherwise the MMAP event may
// not be recorded by perf (see perf_event_open mmap_data).
// But on Android, don't make the mapping executable, because doing so can
// make the mmap call fail on some Android devices. It's also not required on
// Android because simpleperf sets mmap_data = 1 for unrelated reasons (it
// wants to know about vdex files for Java JIT profiling, see
// SetRecordNotExecutableMaps).
int protection = PROT_READ;
# ifndef ANDROID
protection |= PROT_EXEC;
# endif
// Mmap just the first page - that's enough to ensure the path makes it into
// the recording.
long page_size = sysconf(_SC_PAGESIZE);
void* mmap_address = mmap(nullptr, page_size, protection, MAP_PRIVATE, fd, 0);
if (mmap_address == MAP_FAILED) {
fclose(sMarkerFile);
sMarkerFile = nullptr;
return false;
}
#else
// On macOS, we just need to `open` or `fopen` the marker file, and samply
// will know its path because it hooks those functions - no mmap needed.
// On Windows, there's no need to use MOZ_USE_PERFORMANCE_MARKER_FILE because
// we have ETW trace events for UserTiming measures. Still, we want this code
// to compile successfully on Windows, so we use fopen rather than
// open+fdopen.
sMarkerFile = fopen(GetMarkerFilename().c_str(), "w+");
if (!sMarkerFile) {
return false;
}
#endif
return true;
}
// This emits markers to an external marker-[pid].txt file for use by an
// external profiler like samply or etw-gecko
void Performance::MaybeEmitExternalProfilerMarker(
const nsAString& aName, Maybe<const PerformanceMeasureOptions&> aOptions,
Maybe<const nsAString&> aStartMark, const Optional<nsAString>& aEndMark) {
if (!MaybeOpenMarkerFile()) {
return;
}
#if defined(XP_LINUX) || defined(XP_WIN) || defined(XP_MACOSX)
ErrorResult rv;
auto [startTimeStamp, endTimeStamp] =
GetTimeStampsForMarker(aStartMark, aEndMark, aOptions, rv);
if (NS_WARN_IF(rv.Failed())) {
return;
}
#endif
#ifdef XP_LINUX
uint64_t rawStart = startTimeStamp.RawClockMonotonicNanosecondsSinceBoot();
uint64_t rawEnd = endTimeStamp.RawClockMonotonicNanosecondsSinceBoot();
#elif XP_WIN
uint64_t rawStart = startTimeStamp.RawQueryPerformanceCounterValue().value();
uint64_t rawEnd = endTimeStamp.RawQueryPerformanceCounterValue().value();
#elif XP_MACOSX
uint64_t rawStart = startTimeStamp.RawMachAbsoluteTimeNanoseconds();
uint64_t rawEnd = endTimeStamp.RawMachAbsoluteTimeNanoseconds();
#else
uint64_t rawStart = 0;
uint64_t rawEnd = 0;
MOZ_CRASH("no timestamp");
#endif
// Write a line for this measure to the marker file. The marker file uses a
// text-based format where every line is one marker, and each line has the
// format:
// `<raw_start_timestamp> <raw_end_timestamp> <measure_name>`
//
// The timestamp value is OS specific.
fprintf(sMarkerFile, "%" PRIu64 " %" PRIu64 " %s\n", rawStart, rawEnd,
NS_ConvertUTF16toUTF8(aName).get());
fflush(sMarkerFile);
}
already_AddRefed<PerformanceMeasure> Performance::Measure(
JSContext* aCx, const nsAString& aName,
const StringOrPerformanceMeasureOptions& aStartOrMeasureOptions,
const Optional<nsAString>& aEndMark, ErrorResult& aRv) {
if (!GetParentObject()) {
aRv.ThrowInvalidStateError("Global object is unavailable");
return nullptr;
}
// Maybe is more readable than using the union type directly.
Maybe<const PerformanceMeasureOptions&> 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(
"Cannot provide separate endMark argument if "
"PerformanceMeasureOptions argument is given");
return nullptr;
}
if (!options->mStart.WasPassed() && !options->mEnd.WasPassed()) {
aRv.ThrowTypeError(
"PerformanceMeasureOptions must have start and/or end member");
return nullptr;
}
if (options->mStart.WasPassed() && options->mDuration.WasPassed() &&
options->mEnd.WasPassed()) {
aRv.ThrowTypeError(
"PerformanceMeasureOptions cannot have all of the following members: "
"start, duration, and end");
return nullptr;
}
}
const DOMHighResTimeStamp endTime = ResolveEndTimeForMeasure(
aEndMark, options, aRv, /* aReturnUnclamped */ false);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
// Convert to Maybe for consistency with options.
Maybe<const nsAString&> startMark;
if (aStartOrMeasureOptions.IsString()) {
startMark.emplace(aStartOrMeasureOptions.GetAsString());
}
const DOMHighResTimeStamp startTime = ResolveStartTimeForMeasure(
startMark, options, aRv, /* aReturnUnclamped */ false);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
JS::Rooted<JS::Value> detail(aCx);
if (options && !options->mDetail.isNullOrUndefined()) {
StructuredSerializeOptions serializeOptions;
JS::Rooted<JS::Value> valueToClone(aCx, options->mDetail);
nsContentUtils::StructuredClone(aCx, GetParentObject(), valueToClone,
serializeOptions, &detail, aRv);
if (aRv.Failed()) {
return nullptr;
}
} else {
detail.setNull();
}
#ifdef MOZ_PERFETTO
// Perfetto requires that events are properly nested within each category.
// Since this is not a guarantee here, we need to define a dynamic category
// for each measurement so it's not prematurely ended by another measurement
// that overlaps. We also use the usertiming category to guard these markers
// so it's easy to toggle.
if (TRACE_EVENT_CATEGORY_ENABLED("usertiming")) {
NS_ConvertUTF16toUTF8 str(aName);
perfetto::DynamicCategory category{str.get()};
TimeStamp startTimeStamp =
CreationTimeStamp() + TimeDuration::FromMilliseconds(startTime);
TimeStamp endTimeStamp =
CreationTimeStamp() + TimeDuration::FromMilliseconds(endTime);
PERFETTO_TRACE_EVENT_BEGIN(category, perfetto::DynamicString{str.get()},
startTimeStamp);
PERFETTO_TRACE_EVENT_END(category, endTimeStamp);
}
#endif
RefPtr<PerformanceMeasure> performanceMeasure = new PerformanceMeasure(
GetParentObject(), aName, startTime, endTime, detail);
InsertUserEntry(performanceMeasure);
MaybeEmitExternalProfilerMarker(aName, options, startMark, aEndMark);
if (profiler_thread_is_being_profiled_for_markers()) {
auto [startTimeStamp, endTimeStamp] =
GetTimeStampsForMarker(startMark, aEndMark, options, aRv);
Maybe<nsString> endMark;
if (aEndMark.WasPassed()) {
endMark.emplace(aEndMark.Value());
}
Maybe<uint64_t> innerWindowId;
if (nsGlobalWindowInner* owner = GetOwnerWindow()) {
innerWindowId = Some(owner->WindowID());
}
profiler_add_marker("UserTiming", geckoprofiler::category::DOM,
{MarkerTiming::Interval(startTimeStamp, endTimeStamp),
MarkerInnerWindowId(innerWindowId)},
UserTimingMarker{}, aName, /* aIsMeasure */ true,
startMark, endMark);
}
return performanceMeasure.forget();
}
void Performance::ClearMeasures(const Optional<nsAString>& aName) {
ClearUserEntries(aName, u"measure"_ns);
}
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()->GetUTF16String()).get(),
NS_ConvertUTF16toUTF8(aEntry->GetName()->GetUTF16String()).get(),
aEntry->StartTime(), aEntry->Duration(),
static_cast<uint64_t>(PR_Now() / PR_USEC_PER_MSEC));
}
void Performance::TimingNotification(PerformanceEntry* aEntry,
const nsACString& aOwner,
const double aEpoch) {
PerformanceEntryEventInit init;
init.mBubbles = false;
init.mCancelable = false;
aEntry->GetName(init.mName);
aEntry->GetEntryType(init.mEntryType);
init.mStartTime = aEntry->StartTime();
init.mDuration = aEntry->Duration();
init.mEpoch = aEpoch;
CopyUTF8toUTF16(aOwner, init.mOrigin);
RefPtr<PerformanceEntryEvent> perfEntryEvent =
PerformanceEntryEvent::Constructor(this, u"performanceentry"_ns, init);
if (RefPtr<nsGlobalWindowInner> owner = GetOwnerWindow()) {
owner->DispatchEvent(*perfEntryEvent);
}
}
void Performance::InsertUserEntry(PerformanceEntry* aEntry) {
mUserEntries.InsertElementSorted(aEntry, PerformanceEntryComparator());
QueueEntry(aEntry);
}
/*
* Steps are labeled according to the description found at
*
* Buffer Full Event
*/
void Performance::BufferEvent() {
/*
* While resource timing secondary buffer is not empty,
* run the following substeps:
*/
while (!mSecondaryResourceEntries.IsEmpty()) {
uint32_t secondaryResourceEntriesBeforeCount = 0;
uint32_t secondaryResourceEntriesAfterCount = 0;
/*
* Let number of excess entries before be resource
* timing secondary buffer current size.
*/
secondaryResourceEntriesBeforeCount = mSecondaryResourceEntries.Length();
/*
* If can add resource timing entry returns false,
* then fire an event named resourcetimingbufferfull
* at the Performance object.
*/
if (!CanAddResourceTimingEntry()) {
DispatchBufferFullEvent();
}
/*
* Run copy secondary buffer.
*
* While resource timing secondary buffer is not
* empty and can add resource timing entry returns
* true ...
*/
while (!mSecondaryResourceEntries.IsEmpty() &&
CanAddResourceTimingEntry()) {
/*
* Let entry be the oldest PerformanceResourceTiming
* in resource timing secondary buffer. Add entry to
* the end of performance entry buffer. Increment
* resource timing buffer current size by 1.
*/
mResourceEntries.InsertElementSorted(
mSecondaryResourceEntries.ElementAt(0), PerformanceEntryComparator());
/*
* Remove entry from resource timing secondary buffer.
* Decrement resource timing secondary buffer current
* size by 1.
*/
mSecondaryResourceEntries.RemoveElementAt(0);
}
/*
* Let number of excess entries after be resource
* timing secondary buffer current size.
*/
secondaryResourceEntriesAfterCount = mSecondaryResourceEntries.Length();
/*
* If number of excess entries before is lower than
* or equals number of excess entries after, then
* remove all entries from resource timing secondary
* buffer, set resource timing secondary buffer current
* size to 0, and abort these steps.
*/
if (secondaryResourceEntriesBeforeCount <=
secondaryResourceEntriesAfterCount) {
mSecondaryResourceEntries.Clear();
break;
}
}
/*
* Set resource timing buffer full event pending flag
* to false.
*/
mPendingResourceTimingBufferFullEvent = false;
}
void Performance::SetResourceTimingBufferSize(uint64_t aMaxSize) {
mResourceTimingBufferSize = aMaxSize;
}
/*
* Steps are labeled according to the description found at
*
* Can Add Resource Timing Entry
*/
MOZ_ALWAYS_INLINE bool Performance::CanAddResourceTimingEntry() {
/*
* If resource timing buffer current size is smaller than resource timing
* buffer size limit, return true. [Otherwise,] [r]eturn false.
*/
return mResourceEntries.Length() < mResourceTimingBufferSize;
}
/*
* Steps are labeled according to the description found at
*
* Add a PerformanceResourceTiming Entry
*/
void Performance::InsertResourceEntry(PerformanceEntry* aEntry) {
MOZ_ASSERT(aEntry);
QueueEntry(aEntry);
/*
* Let new entry be the input PerformanceEntry to be added.
*
* If can add resource timing entry returns true and resource
* timing buffer full event pending flag is false ...
*/
if (CanAddResourceTimingEntry() && !mPendingResourceTimingBufferFullEvent) {
/*
* Add new entry to the performance entry buffer.
* Increase resource timing buffer current size by 1.
*/
mResourceEntries.InsertElementSorted(aEntry, PerformanceEntryComparator());
return;
}
/*
* If resource timing buffer full event pending flag is
* false ...
*/
if (!mPendingResourceTimingBufferFullEvent) {
/*
* Set resource timing buffer full event pending flag
* to true.
*/
mPendingResourceTimingBufferFullEvent = true;
/*
* Queue a task to run fire a buffer full event.
*/
NS_DispatchToCurrentThread(NewCancelableRunnableMethod(
"Performance::BufferEvent", this, &Performance::BufferEvent));
}
/*
* Add new entry to the resource timing secondary buffer.
* Increase resource timing secondary buffer current size
* by 1.
*/
mSecondaryResourceEntries.InsertElementSorted(aEntry,
PerformanceEntryComparator());
}
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, Notify, ());
}
void Performance::CancelNotificationObservers() {
mPendingNotificationObserversTask = false;
}
class NotifyObserversTask final : public CancelableRunnable {
public:
explicit NotifyObserversTask(Performance* aPerformance)
: CancelableRunnable("dom::NotifyObserversTask"),
mPerformance(aPerformance) {
MOZ_ASSERT(mPerformance);
}
// MOZ_CAN_RUN_SCRIPT_BOUNDARY for now until Runnable::Run is
// MOZ_CAN_RUN_SCRIPT.
MOZ_CAN_RUN_SCRIPT_BOUNDARY
NS_IMETHOD Run() override {
MOZ_ASSERT(mPerformance);
RefPtr<Performance> performance(mPerformance);
performance->NotifyObservers();
return NS_OK;
}
nsresult Cancel() override {
mPerformance->CancelNotificationObservers();
mPerformance = nullptr;
return NS_OK;
}
private:
~NotifyObserversTask() = default;
RefPtr<Performance> mPerformance;
};
void Performance::QueueNotificationObserversTask() {
if (!mPendingNotificationObserversTask) {
RunNotificationObserversTask();
}
}
void Performance::RunNotificationObserversTask() {
mPendingNotificationObserversTask = true;
nsCOMPtr<nsIRunnable> task = new NotifyObserversTask(this);
nsresult rv;
if (nsIGlobalObject* global = GetOwnerGlobal()) {
rv = global->Dispatch(task.forget());
} else {
rv = NS_DispatchToCurrentThread(task.forget());
}
if (NS_WARN_IF(NS_FAILED(rv))) {
mPendingNotificationObserversTask = false;
}
}
void Performance::QueueEntry(PerformanceEntry* aEntry) {
nsTObserverArray<PerformanceObserver*> interestedObservers;
if (!mObservers.IsEmpty()) {
const auto [begin, end] = mObservers.NonObservingRange();
std::copy_if(begin, end, MakeBackInserter(interestedObservers),
[aEntry](PerformanceObserver* observer) {
return observer->ObservesTypeOfEntry(aEntry);
});
}
NS_OBSERVER_ARRAY_NOTIFY_XPCOM_OBSERVERS(interestedObservers, QueueEntry,
(aEntry));
aEntry->BufferEntryIfNeeded();
if (!interestedObservers.IsEmpty()) {
QueueNotificationObserversTask();
}
}
// We could clear User entries here, but doing so could break sites that call
// performance.measure() if the marks disappeared without warning. Chrome
// allows "infinite" entries.
void Performance::MemoryPressure() {}
size_t Performance::SizeOfUserEntries(
mozilla::MallocSizeOf aMallocSizeOf) const {
size_t userEntries = 0;
for (const PerformanceEntry* entry : mUserEntries) {
userEntries += entry->SizeOfIncludingThis(aMallocSizeOf);
}
return userEntries;
}
size_t Performance::SizeOfResourceEntries(
mozilla::MallocSizeOf aMallocSizeOf) const {
size_t resourceEntries = 0;
for (const PerformanceEntry* entry : mResourceEntries) {
resourceEntries += entry->SizeOfIncludingThis(aMallocSizeOf);
}
return resourceEntries;
}
} // namespace mozilla::dom