/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- *//* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsPerformanceStats.h" #include "nsMemory.h" #include "nsLiteralString.h" #include "nsCRTGlue.h" #include "nsServiceManagerUtils.h" #include "nsCOMArray.h" #include "nsContentUtils.h" #include "nsIMutableArray.h" #include "nsReadableUtils.h" #include "jsapi.h" #include "nsJSUtils.h" #include "xpcpublic.h" #include "jspubtd.h" #include "nsIDOMWindow.h" #include "nsGlobalWindow.h" #include "nsRefreshDriver.h" #include "mozilla/Unused.h" #include "mozilla/ArrayUtils.h" #include "mozilla/EventStateManager.h" #include "mozilla/Services.h" #if defined(XP_WIN) #include #include #else #include #endif // defined(XP_WIN) #if defined(XP_UNIX) #include #include #endif // defined(XP_UNIX) /* ------------------------------------------------------ * * Utility functions. * */ namespace { /** * Get the private window for the current compartment. * * @return null if the code is not executed in a window or in * case of error, a nsPIDOMWindow otherwise. */ already_AddRefed GetPrivateWindow(JSContext* cx) { nsGlobalWindow* win = xpc::CurrentWindowOrNull(cx); if (!win) { return nullptr; } nsPIDOMWindowOuter* outer = win->AsInner()->GetOuterWindow(); if (!outer) { return nullptr; } nsCOMPtr top = outer->GetTop(); if (!top) { return nullptr; } return top.forget(); } bool URLForGlobal(JSContext* cx, JS::Handle global, nsAString& url) { nsCOMPtr principal = nsContentUtils::ObjectPrincipal(global); if (!principal) { return false; } nsCOMPtr uri; nsresult rv = principal->GetURI(getter_AddRefs(uri)); if (NS_FAILED(rv) || !uri) { return false; } nsAutoCString spec; rv = uri->GetSpec(spec); if (NS_FAILED(rv)) { return false; } url.Assign(NS_ConvertUTF8toUTF16(spec)); return true; } /** * Extract a somewhat human-readable name from the current context. */ void CompartmentName(JSContext* cx, JS::Handle global, nsAString& name) { // Attempt to use the URL as name. if (URLForGlobal(cx, global, name)) { return; } // Otherwise, fallback to XPConnect's less readable but more // complete naming scheme. nsAutoCString cname; xpc::GetCurrentCompartmentName(cx, cname); name.Assign(NS_ConvertUTF8toUTF16(cname)); } /** * Generate a unique-to-the-application identifier for a group. */ void GenerateUniqueGroupId(const JSContext* cx, uint64_t uid, uint64_t processId, nsAString& groupId) { uint64_t contextId = reinterpret_cast(cx); groupId.AssignLiteral("process: "); groupId.AppendInt(processId); groupId.AppendLiteral(", thread: "); groupId.AppendInt(contextId); groupId.AppendLiteral(", group: "); groupId.AppendInt(uid); } static const char* TOPICS[] = { "profile-before-change", "quit-application", "quit-application-granted", "xpcom-will-shutdown" }; } // namespace /* ------------------------------------------------------ * * class nsPerformanceObservationTarget * */ NS_IMPL_ISUPPORTS(nsPerformanceObservationTarget, nsIPerformanceObservable) NS_IMETHODIMP nsPerformanceObservationTarget::GetTarget(nsIPerformanceGroupDetails** _result) { if (mDetails) { NS_IF_ADDREF(*_result = mDetails); } return NS_OK; }; void nsPerformanceObservationTarget::SetTarget(nsPerformanceGroupDetails* details) { MOZ_ASSERT(!mDetails); mDetails = details; }; NS_IMETHODIMP nsPerformanceObservationTarget::AddJankObserver(nsIPerformanceObserver* observer) { if (!mObservers.append(observer)) { MOZ_CRASH(); } return NS_OK; }; NS_IMETHODIMP nsPerformanceObservationTarget::RemoveJankObserver(nsIPerformanceObserver* observer) { for (auto iter = mObservers.begin(), end = mObservers.end(); iter < end; ++iter) { if (*iter == observer) { mObservers.erase(iter); return NS_OK; } } return NS_OK; }; bool nsPerformanceObservationTarget::HasObservers() const { return !mObservers.empty(); } void nsPerformanceObservationTarget::NotifyJankObservers(nsIPerformanceGroupDetails* source, nsIPerformanceAlert* gravity) { // Copy the vector to make sure that it won't change under our feet. mozilla::Vector> observers; if (!observers.appendAll(mObservers)) { MOZ_CRASH(); } // Now actually notify. for (auto iter = observers.begin(), end = observers.end(); iter < end; ++iter) { nsCOMPtr observer = *iter; mozilla::Unused << observer->Observe(source, gravity); } } /* ------------------------------------------------------ * * class nsGroupHolder * */ nsPerformanceObservationTarget* nsGroupHolder::ObservationTarget() { if (!mPendingObservationTarget) { mPendingObservationTarget = new nsPerformanceObservationTarget(); } return mPendingObservationTarget; } nsPerformanceGroup* nsGroupHolder::GetGroup() { return mGroup; } void nsGroupHolder::SetGroup(nsPerformanceGroup* group) { MOZ_ASSERT(!mGroup); mGroup = group; group->SetObservationTarget(ObservationTarget()); mPendingObservationTarget->SetTarget(group->Details()); } /* ------------------------------------------------------ * * struct PerformanceData * */ PerformanceData::PerformanceData() : mTotalUserTime(0) , mTotalSystemTime(0) , mTotalCPOWTime(0) , mTicks(0) { mozilla::PodArrayZero(mDurations); } /* ------------------------------------------------------ * * class nsPerformanceGroupDetails * */ NS_IMPL_ISUPPORTS(nsPerformanceGroupDetails, nsIPerformanceGroupDetails) const nsAString& nsPerformanceGroupDetails::Name() const { return mName; } const nsAString& nsPerformanceGroupDetails::GroupId() const { return mGroupId; } const nsAString& nsPerformanceGroupDetails::AddonId() const { return mAddonId; } uint64_t nsPerformanceGroupDetails::WindowId() const { return mWindowId; } uint64_t nsPerformanceGroupDetails::ProcessId() const { return mProcessId; } bool nsPerformanceGroupDetails::IsSystem() const { return mIsSystem; } bool nsPerformanceGroupDetails::IsAddon() const { return mAddonId.Length() != 0; } bool nsPerformanceGroupDetails::IsWindow() const { return mWindowId != 0; } bool nsPerformanceGroupDetails::IsContentProcess() const { return XRE_GetProcessType() == GeckoProcessType_Content; } /* readonly attribute AString name; */ NS_IMETHODIMP nsPerformanceGroupDetails::GetName(nsAString& aName) { aName.Assign(Name()); return NS_OK; }; /* readonly attribute AString groupId; */ NS_IMETHODIMP nsPerformanceGroupDetails::GetGroupId(nsAString& aGroupId) { aGroupId.Assign(GroupId()); return NS_OK; }; /* readonly attribute AString addonId; */ NS_IMETHODIMP nsPerformanceGroupDetails::GetAddonId(nsAString& aAddonId) { aAddonId.Assign(AddonId()); return NS_OK; }; /* readonly attribute uint64_t windowId; */ NS_IMETHODIMP nsPerformanceGroupDetails::GetWindowId(uint64_t *aWindowId) { *aWindowId = WindowId(); return NS_OK; } /* readonly attribute bool isSystem; */ NS_IMETHODIMP nsPerformanceGroupDetails::GetIsSystem(bool *_retval) { *_retval = IsSystem(); return NS_OK; } /* readonly attribute unsigned long long processId; */ NS_IMETHODIMP nsPerformanceGroupDetails::GetProcessId(uint64_t* processId) { *processId = ProcessId(); return NS_OK; } /* readonly attribute bool IsContentProcess; */ NS_IMETHODIMP nsPerformanceGroupDetails::GetIsContentProcess(bool *_retval) { *_retval = IsContentProcess(); return NS_OK; } /* ------------------------------------------------------ * * class nsPerformanceStats * */ class nsPerformanceStats final: public nsIPerformanceStats { public: NS_DECL_ISUPPORTS NS_DECL_NSIPERFORMANCESTATS NS_FORWARD_NSIPERFORMANCEGROUPDETAILS(mDetails->) nsPerformanceStats(nsPerformanceGroupDetails* item, const PerformanceData& aPerformanceData) : mDetails(item) , mPerformanceData(aPerformanceData) { } private: RefPtr mDetails; PerformanceData mPerformanceData; ~nsPerformanceStats() {} }; NS_IMPL_ISUPPORTS(nsPerformanceStats, nsIPerformanceStats, nsIPerformanceGroupDetails) /* readonly attribute unsigned long long totalUserTime; */ NS_IMETHODIMP nsPerformanceStats::GetTotalUserTime(uint64_t *aTotalUserTime) { *aTotalUserTime = mPerformanceData.mTotalUserTime; return NS_OK; }; /* readonly attribute unsigned long long totalSystemTime; */ NS_IMETHODIMP nsPerformanceStats::GetTotalSystemTime(uint64_t *aTotalSystemTime) { *aTotalSystemTime = mPerformanceData.mTotalSystemTime; return NS_OK; }; /* readonly attribute unsigned long long totalCPOWTime; */ NS_IMETHODIMP nsPerformanceStats::GetTotalCPOWTime(uint64_t *aCpowTime) { *aCpowTime = mPerformanceData.mTotalCPOWTime; return NS_OK; }; /* readonly attribute unsigned long long ticks; */ NS_IMETHODIMP nsPerformanceStats::GetTicks(uint64_t *aTicks) { *aTicks = mPerformanceData.mTicks; return NS_OK; }; /* void getDurations (out unsigned long aCount, [array, size_is (aCount), retval] out unsigned long long aNumberOfOccurrences); */ NS_IMETHODIMP nsPerformanceStats::GetDurations(uint32_t *aCount, uint64_t **aNumberOfOccurrences) { const size_t length = mozilla::ArrayLength(mPerformanceData.mDurations); if (aCount) { *aCount = length; } *aNumberOfOccurrences = new uint64_t[length]; for (size_t i = 0; i < length; ++i) { (*aNumberOfOccurrences)[i] = mPerformanceData.mDurations[i]; } return NS_OK; }; /* ------------------------------------------------------ * * struct nsPerformanceSnapshot * */ class nsPerformanceSnapshot final : public nsIPerformanceSnapshot { public: NS_DECL_ISUPPORTS NS_DECL_NSIPERFORMANCESNAPSHOT nsPerformanceSnapshot() {} /** * Append statistics to the list of components data. */ void AppendComponentsStats(nsIPerformanceStats* stats); /** * Set the statistics attached to process data. */ void SetProcessStats(nsIPerformanceStats* group); private: ~nsPerformanceSnapshot() {} private: /** * The data for all components. */ nsCOMArray mComponentsData; /** * The data for the process. */ nsCOMPtr mProcessData; }; NS_IMPL_ISUPPORTS(nsPerformanceSnapshot, nsIPerformanceSnapshot) /* nsIArray getComponentsData (); */ NS_IMETHODIMP nsPerformanceSnapshot::GetComponentsData(nsIArray * *aComponents) { const size_t length = mComponentsData.Length(); nsCOMPtr components = do_CreateInstance(NS_ARRAY_CONTRACTID); for (size_t i = 0; i < length; ++i) { nsCOMPtr stats = mComponentsData[i]; mozilla::DebugOnly rv = components->AppendElement(stats, false); MOZ_ASSERT(NS_SUCCEEDED(rv)); } components.forget(aComponents); return NS_OK; } /* nsIPerformanceStats getProcessData (); */ NS_IMETHODIMP nsPerformanceSnapshot::GetProcessData(nsIPerformanceStats * *aProcess) { NS_IF_ADDREF(*aProcess = mProcessData); return NS_OK; } void nsPerformanceSnapshot::AppendComponentsStats(nsIPerformanceStats* stats) { mComponentsData.AppendElement(stats); } void nsPerformanceSnapshot::SetProcessStats(nsIPerformanceStats* stats) { mProcessData = stats; } /* ------------------------------------------------------ * * class PerformanceAlert * */ class PerformanceAlert final: public nsIPerformanceAlert { public: NS_DECL_ISUPPORTS NS_DECL_NSIPERFORMANCEALERT PerformanceAlert(const uint32_t reason, nsPerformanceGroup* source); private: ~PerformanceAlert() {} const uint32_t mReason; // The highest values reached by this group since the latest alert, // in microseconds. const uint64_t mHighestJank; const uint64_t mHighestCPOW; }; NS_IMPL_ISUPPORTS(PerformanceAlert, nsIPerformanceAlert); PerformanceAlert::PerformanceAlert(const uint32_t reason, nsPerformanceGroup* source) : mReason(reason) , mHighestJank(source->HighestRecentJank()) , mHighestCPOW(source->HighestRecentCPOW()) { } NS_IMETHODIMP PerformanceAlert::GetHighestJank(uint64_t* result) { *result = mHighestJank; return NS_OK; } NS_IMETHODIMP PerformanceAlert::GetHighestCPOW(uint64_t* result) { *result = mHighestCPOW; return NS_OK; } NS_IMETHODIMP PerformanceAlert::GetReason(uint32_t* result) { *result = mReason; return NS_OK; } /* ------------------------------------------------------ * * class PendingAlertsCollector * */ /** * A timer callback in charge of collecting the groups in * `mPendingAlerts` and triggering dispatch of performance alerts. */ class PendingAlertsCollector final: public nsITimerCallback { public: NS_DECL_ISUPPORTS NS_DECL_NSITIMERCALLBACK explicit PendingAlertsCollector(nsPerformanceStatsService* service) : mService(service) , mPending(false) { } nsresult Start(uint32_t timerDelayMS); nsresult Dispose(); private: ~PendingAlertsCollector() {} RefPtr mService; bool mPending; nsCOMPtr mTimer; mozilla::Vector mJankLevels; }; NS_IMPL_ISUPPORTS(PendingAlertsCollector, nsITimerCallback); NS_IMETHODIMP PendingAlertsCollector::Notify(nsITimer*) { mPending = false; mService->NotifyJankObservers(mJankLevels); return NS_OK; } nsresult PendingAlertsCollector::Start(uint32_t timerDelayMS) { if (mPending) { // Collector is already started. return NS_OK; } if (!mTimer) { mTimer = do_CreateInstance(NS_TIMER_CONTRACTID); } nsresult rv = mTimer->InitWithCallback(this, timerDelayMS, nsITimer::TYPE_ONE_SHOT); if (NS_FAILED(rv)) { return rv; } mPending = true; { mozilla::DebugOnly result = nsRefreshDriver::GetJankLevels(mJankLevels); MOZ_ASSERT(result); } return NS_OK; } nsresult PendingAlertsCollector::Dispose() { if (mTimer) { mozilla::Unused << mTimer->Cancel(); mTimer = nullptr; } mService = nullptr; return NS_OK; } /* ------------------------------------------------------ * * class nsPerformanceStatsService * */ NS_IMPL_ISUPPORTS(nsPerformanceStatsService, nsIPerformanceStatsService, nsIObserver) nsPerformanceStatsService::nsPerformanceStatsService() : mIsAvailable(false) , mDisposed(false) #if defined(XP_WIN) , mProcessId(GetCurrentProcessId()) #else , mProcessId(getpid()) #endif , mContext(mozilla::dom::danger::GetJSContext()) , mUIdCounter(0) , mTopGroup(nsPerformanceGroup::Make(mContext, this, NS_LITERAL_STRING(""), // name NS_LITERAL_STRING(""), // addonid 0, // windowId mProcessId, true, // isSystem nsPerformanceGroup::GroupScope::RUNTIME // scope )) , mIsHandlingUserInput(false) , mIsMonitoringPerCompartment(false) , mJankAlertThreshold(mozilla::MaxValue::value) // By default, no alerts , mJankAlertBufferingDelay(1000 /* ms */) , mJankLevelVisibilityThreshold(/* 2 ^ */ 8 /* ms */) , mMaxExpectedDurationOfInteractionUS(150 * 1000) { mPendingAlertsCollector = new PendingAlertsCollector(this); // Attach some artificial group information to the universal listeners, to aid with debugging. nsString groupIdForAddons; GenerateUniqueGroupId(mContext, GetNextId(), mProcessId, groupIdForAddons); mUniversalTargets.mAddons-> SetTarget(new nsPerformanceGroupDetails(NS_LITERAL_STRING(""), groupIdForAddons, NS_LITERAL_STRING(""), 0, // window id mProcessId, false)); nsString groupIdForWindows; GenerateUniqueGroupId(mContext, GetNextId(), mProcessId, groupIdForWindows); mUniversalTargets.mWindows-> SetTarget(new nsPerformanceGroupDetails(NS_LITERAL_STRING(""), groupIdForWindows, NS_LITERAL_STRING(""), 0, // window id mProcessId, false)); } nsPerformanceStatsService::~nsPerformanceStatsService() { } /** * Clean up the service. * * Called during shutdown. Idempotent. */ void nsPerformanceStatsService::Dispose() { // Make sure that we do not accidentally destroy `this` while we are // cleaning up back references. RefPtr kungFuDeathGrip(this); mIsAvailable = false; if (mDisposed) { // Make sure that we don't double-dispose. return; } mDisposed = true; // Disconnect from nsIObserverService. nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { for (size_t i = 0; i < mozilla::ArrayLength(TOPICS); ++i) { mozilla::Unused << obs->RemoveObserver(this, TOPICS[i]); } } // Clear up and disconnect from JSAPI. JSContext* cx = mContext; js::DisposePerformanceMonitoring(cx); mozilla::Unused << js::SetStopwatchIsMonitoringCPOW(cx, false); mozilla::Unused << js::SetStopwatchIsMonitoringJank(cx, false); mozilla::Unused << js::SetStopwatchStartCallback(cx, nullptr, nullptr); mozilla::Unused << js::SetStopwatchCommitCallback(cx, nullptr, nullptr); mozilla::Unused << js::SetGetPerformanceGroupsCallback(cx, nullptr, nullptr); // Clear up and disconnect the alerts collector. if (mPendingAlertsCollector) { mPendingAlertsCollector->Dispose(); mPendingAlertsCollector = nullptr; } mPendingAlerts.clear(); // Disconnect universal observers. Per-group observers will be // disconnected below as part of `group->Dispose()`. mUniversalTargets.mAddons = nullptr; mUniversalTargets.mWindows = nullptr; // At this stage, the JS VM may still be holding references to // instances of PerformanceGroup on the stack. To let the service be // collected, we need to break the references from these groups to // `this`. mTopGroup->Dispose(); mTopGroup = nullptr; // Copy references to the groups to a vector to ensure that we do // not modify the hashtable while iterating it. GroupVector groups; for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) { if (!groups.append(iter.Get()->GetKey())) { MOZ_CRASH(); } } for (auto iter = groups.begin(), end = groups.end(); iter < end; ++iter) { RefPtr group = *iter; group->Dispose(); } // Any remaining references to PerformanceGroup will be released as // the VM unrolls the stack. If there are any nested event loops, // this may take time. } nsresult nsPerformanceStatsService::Init() { nsresult rv = InitInternal(); if (NS_FAILED(rv)) { // Attempt to clean up. Dispose(); } return rv; } nsresult nsPerformanceStatsService::InitInternal() { // Make sure that we release everything during shutdown. // We are a bit defensive here, as we know that some strange behavior can break the // regular shutdown order. nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { for (size_t i = 0; i < mozilla::ArrayLength(TOPICS); ++i) { mozilla::Unused << obs->AddObserver(this, TOPICS[i], false); } } // Connect to JSAPI. JSContext* cx = mContext; if (!js::SetStopwatchStartCallback(cx, StopwatchStartCallback, this)) { return NS_ERROR_UNEXPECTED; } if (!js::SetStopwatchCommitCallback(cx, StopwatchCommitCallback, this)) { return NS_ERROR_UNEXPECTED; } if (!js::SetGetPerformanceGroupsCallback(cx, GetPerformanceGroupsCallback, this)) { return NS_ERROR_UNEXPECTED; } mTopGroup->setIsActive(true); mIsAvailable = true; return NS_OK; } // Observe shutdown events. NS_IMETHODIMP nsPerformanceStatsService::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { MOZ_ASSERT(strcmp(aTopic, "profile-before-change") == 0 || strcmp(aTopic, "quit-application") == 0 || strcmp(aTopic, "quit-application-granted") == 0 || strcmp(aTopic, "xpcom-will-shutdown") == 0); Dispose(); return NS_OK; } /*static*/ bool nsPerformanceStatsService::IsHandlingUserInput() { if (mozilla::EventStateManager::LatestUserInputStart().IsNull()) { return false; } bool result = mozilla::TimeStamp::Now() - mozilla::EventStateManager::LatestUserInputStart() <= mozilla::TimeDuration::FromMicroseconds(mMaxExpectedDurationOfInteractionUS); return result; } /* [implicit_jscontext] attribute bool isMonitoringCPOW; */ NS_IMETHODIMP nsPerformanceStatsService::GetIsMonitoringCPOW(JSContext* cx, bool *aIsStopwatchActive) { if (!mIsAvailable) { return NS_ERROR_NOT_AVAILABLE; } *aIsStopwatchActive = js::GetStopwatchIsMonitoringCPOW(cx); return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::SetIsMonitoringCPOW(JSContext* cx, bool aIsStopwatchActive) { if (!mIsAvailable) { return NS_ERROR_NOT_AVAILABLE; } if (!js::SetStopwatchIsMonitoringCPOW(cx, aIsStopwatchActive)) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; } /* [implicit_jscontext] attribute bool isMonitoringJank; */ NS_IMETHODIMP nsPerformanceStatsService::GetIsMonitoringJank(JSContext* cx, bool *aIsStopwatchActive) { if (!mIsAvailable) { return NS_ERROR_NOT_AVAILABLE; } *aIsStopwatchActive = js::GetStopwatchIsMonitoringJank(cx); return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::SetIsMonitoringJank(JSContext* cx, bool aIsStopwatchActive) { if (!mIsAvailable) { return NS_ERROR_NOT_AVAILABLE; } if (!js::SetStopwatchIsMonitoringJank(cx, aIsStopwatchActive)) { return NS_ERROR_OUT_OF_MEMORY; } return NS_OK; } /* [implicit_jscontext] attribute bool isMonitoringPerCompartment; */ NS_IMETHODIMP nsPerformanceStatsService::GetIsMonitoringPerCompartment(JSContext*, bool *aIsMonitoringPerCompartment) { if (!mIsAvailable) { return NS_ERROR_NOT_AVAILABLE; } *aIsMonitoringPerCompartment = mIsMonitoringPerCompartment; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::SetIsMonitoringPerCompartment(JSContext*, bool aIsMonitoringPerCompartment) { if (!mIsAvailable) { return NS_ERROR_NOT_AVAILABLE; } if (aIsMonitoringPerCompartment == mIsMonitoringPerCompartment) { return NS_OK; } // Relatively slow update: walk the entire lost of performance groups, // update the active flag of those that have changed. // // Alternative strategies could be envisioned to make the update // much faster, at the expense of the speed of calling `isActive()`, // (e.g. deferring `isActive()` to the nsPerformanceStatsService), // but we expect that `isActive()` can be called thousands of times // per second, while `SetIsMonitoringPerCompartment` is not called // at all during most Firefox runs. for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) { RefPtr group = iter.Get()->GetKey(); if (group->Scope() == nsPerformanceGroup::GroupScope::COMPARTMENT) { group->setIsActive(aIsMonitoringPerCompartment); } } mIsMonitoringPerCompartment = aIsMonitoringPerCompartment; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::GetJankAlertThreshold(uint64_t* result) { *result = mJankAlertThreshold; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::SetJankAlertThreshold(uint64_t value) { mJankAlertThreshold = value; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::GetJankAlertBufferingDelay(uint32_t* result) { *result = mJankAlertBufferingDelay; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::SetJankAlertBufferingDelay(uint32_t value) { mJankAlertBufferingDelay = value; return NS_OK; } /* static */ nsIPerformanceStats* nsPerformanceStatsService::GetStatsForGroup(const js::PerformanceGroup* group) { return GetStatsForGroup(nsPerformanceGroup::Get(group)); } /* static */ nsIPerformanceStats* nsPerformanceStatsService::GetStatsForGroup(const nsPerformanceGroup* group) { return new nsPerformanceStats(group->Details(), group->data); } /* [implicit_jscontext] nsIPerformanceSnapshot getSnapshot (); */ NS_IMETHODIMP nsPerformanceStatsService::GetSnapshot(JSContext* cx, nsIPerformanceSnapshot * *aSnapshot) { if (!mIsAvailable) { return NS_ERROR_NOT_AVAILABLE; } RefPtr snapshot = new nsPerformanceSnapshot(); snapshot->SetProcessStats(GetStatsForGroup(mTopGroup)); for (auto iter = mGroups.Iter(); !iter.Done(); iter.Next()) { auto* entry = iter.Get(); nsPerformanceGroup* group = entry->GetKey(); if (group->isActive()) { snapshot->AppendComponentsStats(GetStatsForGroup(group)); } } snapshot.forget(aSnapshot); return NS_OK; } uint64_t nsPerformanceStatsService::GetNextId() { return ++mUIdCounter; } /* static*/ bool nsPerformanceStatsService::GetPerformanceGroupsCallback(JSContext* cx, js::PerformanceGroupVector& out, void* closure) { RefPtr self = reinterpret_cast(closure); return self->GetPerformanceGroups(cx, out); } bool nsPerformanceStatsService::GetPerformanceGroups(JSContext* cx, js::PerformanceGroupVector& out) { JS::RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); if (!global) { // While it is possible for a compartment to have no global // (e.g. atoms), this compartment is not very interesting for us. return true; } // All compartments belong to the top group. if (!out.append(mTopGroup)) { JS_ReportOutOfMemory(cx); return false; } nsAutoString name; CompartmentName(cx, global, name); bool isSystem = nsContentUtils::IsSystemPrincipal(nsContentUtils::ObjectPrincipal(global)); // Find out if the compartment is executed by an add-on. If so, its // duration should count towards the total duration of the add-on. JSAddonId* jsaddonId = AddonIdOfObject(global); nsString addonId; if (jsaddonId) { AssignJSFlatString(addonId, (JSFlatString*)jsaddonId); auto entry = mAddonIdToGroup.PutEntry(addonId); if (!entry->GetGroup()) { nsString addonName = name; addonName.AppendLiteral(" (as addon "); addonName.Append(addonId); addonName.AppendLiteral(")"); entry-> SetGroup(nsPerformanceGroup::Make(mContext, this, addonName, addonId, 0, mProcessId, isSystem, nsPerformanceGroup::GroupScope::ADDON) ); } if (!out.append(entry->GetGroup())) { JS_ReportOutOfMemory(cx); return false; } } // Find out if the compartment is executed by a window. If so, its // duration should count towards the total duration of the window. uint64_t windowId = 0; if (nsCOMPtr ptop = GetPrivateWindow(cx)) { windowId = ptop->WindowID(); auto entry = mWindowIdToGroup.PutEntry(windowId); if (!entry->GetGroup()) { nsString windowName = name; windowName.AppendLiteral(" (as window "); windowName.AppendInt(windowId); windowName.AppendLiteral(")"); entry-> SetGroup(nsPerformanceGroup::Make(mContext, this, windowName, EmptyString(), windowId, mProcessId, isSystem, nsPerformanceGroup::GroupScope::WINDOW) ); } if (!out.append(entry->GetGroup())) { JS_ReportOutOfMemory(cx); return false; } } // All compartments have their own group. auto group = nsPerformanceGroup::Make(mContext, this, name, addonId, windowId, mProcessId, isSystem, nsPerformanceGroup::GroupScope::COMPARTMENT); if (!out.append(group)) { JS_ReportOutOfMemory(cx); return false; } // Returning a vector that is too large would cause allocations all over the // place in the JS engine. We want to be sure that all data is stored inline. MOZ_ASSERT(out.length() <= out.sMaxInlineStorage); return true; } /*static*/ bool nsPerformanceStatsService::StopwatchStartCallback(uint64_t iteration, void* closure) { RefPtr self = reinterpret_cast(closure); return self->StopwatchStart(iteration); } bool nsPerformanceStatsService::StopwatchStart(uint64_t iteration) { mIteration = iteration; mIsHandlingUserInput = IsHandlingUserInput(); mUserInputCount = mozilla::EventStateManager::UserInputCount(); nsresult rv = GetResources(&mUserTimeStart, &mSystemTimeStart); if (NS_FAILED(rv)) { return false; } return true; } /*static*/ bool nsPerformanceStatsService::StopwatchCommitCallback(uint64_t iteration, js::PerformanceGroupVector& recentGroups, void* closure) { RefPtr self = reinterpret_cast(closure); return self->StopwatchCommit(iteration, recentGroups); } bool nsPerformanceStatsService::StopwatchCommit(uint64_t iteration, js::PerformanceGroupVector& recentGroups) { MOZ_ASSERT(iteration == mIteration); MOZ_ASSERT(!recentGroups.empty()); uint64_t userTimeStop, systemTimeStop; nsresult rv = GetResources(&userTimeStop, &systemTimeStop); if (NS_FAILED(rv)) { return false; } // `GetResources` is not guaranteed to be monotonic, so round up // any negative result to 0 milliseconds. uint64_t userTimeDelta = 0; if (userTimeStop > mUserTimeStart) userTimeDelta = userTimeStop - mUserTimeStart; uint64_t systemTimeDelta = 0; if (systemTimeStop > mSystemTimeStart) systemTimeDelta = systemTimeStop - mSystemTimeStart; MOZ_ASSERT(mTopGroup->isUsedInThisIteration()); const uint64_t totalRecentCycles = mTopGroup->recentCycles(iteration); const bool isHandlingUserInput = mIsHandlingUserInput || mozilla::EventStateManager::UserInputCount() > mUserInputCount; // We should only reach this stage if `group` has had some activity. MOZ_ASSERT(mTopGroup->recentTicks(iteration) > 0); for (auto iter = recentGroups.begin(), end = recentGroups.end(); iter != end; ++iter) { RefPtr group = nsPerformanceGroup::Get(*iter); CommitGroup(iteration, userTimeDelta, systemTimeDelta, totalRecentCycles, isHandlingUserInput, group); } // Make sure that `group` was treated along with the other items of `recentGroups`. MOZ_ASSERT(!mTopGroup->isUsedInThisIteration()); MOZ_ASSERT(mTopGroup->recentTicks(iteration) == 0); if (!mPendingAlerts.empty()) { mPendingAlertsCollector->Start(mJankAlertBufferingDelay); } return true; } void nsPerformanceStatsService::CommitGroup(uint64_t iteration, uint64_t totalUserTimeDelta, uint64_t totalSystemTimeDelta, uint64_t totalCyclesDelta, bool isHandlingUserInput, nsPerformanceGroup* group) { MOZ_ASSERT(group->isUsedInThisIteration()); const uint64_t ticksDelta = group->recentTicks(iteration); const uint64_t cpowTimeDelta = group->recentCPOW(iteration); const uint64_t cyclesDelta = group->recentCycles(iteration); group->resetRecentData(); // We have now performed all cleanup and may `return` at any time without fear of leaks. if (group->iteration() != iteration) { // Stale data, don't commit. return; } // When we add a group as changed, we immediately set its // `recentTicks` from 0 to 1. If we have `ticksDelta == 0` at // this stage, we have already called `resetRecentData` but we // haven't removed it from the list. MOZ_ASSERT(ticksDelta != 0); MOZ_ASSERT(cyclesDelta <= totalCyclesDelta); if (cyclesDelta == 0 || totalCyclesDelta == 0) { // Nothing useful, don't commit. return; } double proportion = (double)cyclesDelta / (double)totalCyclesDelta; MOZ_ASSERT(proportion <= 1); const uint64_t userTimeDelta = proportion * totalUserTimeDelta; const uint64_t systemTimeDelta = proportion * totalSystemTimeDelta; group->data.mTotalUserTime += userTimeDelta; group->data.mTotalSystemTime += systemTimeDelta; group->data.mTotalCPOWTime += cpowTimeDelta; group->data.mTicks += ticksDelta; const uint64_t totalTimeDelta = userTimeDelta + systemTimeDelta + cpowTimeDelta; uint64_t duration = 1000; // 1ms in µs for (size_t i = 0; i < mozilla::ArrayLength(group->data.mDurations) && duration < totalTimeDelta; ++i, duration *= 2) { group->data.mDurations[i]++; } group->RecordJank(totalTimeDelta); group->RecordCPOW(cpowTimeDelta); if (isHandlingUserInput) { group->RecordUserInput(); } if (totalTimeDelta >= mJankAlertThreshold) { if (!group->HasPendingAlert()) { if (mPendingAlerts.append(group)) { group->SetHasPendingAlert(true); } return; } } return; } nsresult nsPerformanceStatsService::GetResources(uint64_t* userTime, uint64_t* systemTime) const { MOZ_ASSERT(userTime); MOZ_ASSERT(systemTime); #if defined(XP_UNIX) struct rusage rusage; #if defined(RUSAGE_THREAD) // Under Linux, we can obtain per-thread statistics int err = getrusage(RUSAGE_THREAD, &rusage); #else // Under other Unices, we need to do with more noisy // per-process statistics. int err = getrusage(RUSAGE_SELF, &rusage); #endif // defined(RUSAGE_THREAD) if (err) return NS_ERROR_FAILURE; *userTime = rusage.ru_utime.tv_usec + rusage.ru_utime.tv_sec * 1000000; *systemTime = rusage.ru_stime.tv_usec + rusage.ru_stime.tv_sec * 1000000; #elif defined(XP_WIN) // Under Windows, we can obtain per-thread statistics. Experience // seems to suggest that they are not very accurate under Windows // XP, though. FILETIME creationFileTime; // Ignored FILETIME exitFileTime; // Ignored FILETIME kernelFileTime; FILETIME userFileTime; BOOL success = GetThreadTimes(GetCurrentThread(), &creationFileTime, &exitFileTime, &kernelFileTime, &userFileTime); if (!success) return NS_ERROR_FAILURE; ULARGE_INTEGER kernelTimeInt; kernelTimeInt.LowPart = kernelFileTime.dwLowDateTime; kernelTimeInt.HighPart = kernelFileTime.dwHighDateTime; // Convert 100 ns to 1 us. *systemTime = kernelTimeInt.QuadPart / 10; ULARGE_INTEGER userTimeInt; userTimeInt.LowPart = userFileTime.dwLowDateTime; userTimeInt.HighPart = userFileTime.dwHighDateTime; // Convert 100 ns to 1 us. *userTime = userTimeInt.QuadPart / 10; #endif // defined(XP_UNIX) || defined(XP_WIN) return NS_OK; } void nsPerformanceStatsService::NotifyJankObservers(const mozilla::Vector& aPreviousJankLevels) { // The move operation is generally constant time, unless // `mPendingAlerts.length()` is very small, in which case it's fast anyway. GroupVector alerts(Move(mPendingAlerts)); mPendingAlerts = GroupVector(); // Reconstruct after `Move`. if (!mPendingAlertsCollector) { // We are shutting down. return; } // Find out if we have noticed any user-noticeable delay in an // animation recently (i.e. since the start of the execution of JS // code that caused this collector to start). If so, we'll mark any // alert as part of a user-noticeable jank. Note that this doesn't // mean with any certainty that the alert is the only cause of jank, // or even the main cause of jank. mozilla::Vector latestJankLevels; { mozilla::DebugOnly result = nsRefreshDriver::GetJankLevels(latestJankLevels); MOZ_ASSERT(result); } MOZ_ASSERT(latestJankLevels.length() == aPreviousJankLevels.length()); bool isJankInAnimation = false; for (size_t i = mJankLevelVisibilityThreshold; i < latestJankLevels.length(); ++i) { if (latestJankLevels[i] > aPreviousJankLevels[i]) { isJankInAnimation = true; break; } } MOZ_ASSERT(!alerts.empty()); const bool hasUniversalAddonObservers = mUniversalTargets.mAddons->HasObservers(); const bool hasUniversalWindowObservers = mUniversalTargets.mWindows->HasObservers(); for (auto iter = alerts.begin(); iter < alerts.end(); ++iter) { MOZ_ASSERT(iter); RefPtr group = *iter; group->SetHasPendingAlert(false); RefPtr details = group->Details(); nsPerformanceObservationTarget* targets[3] = { hasUniversalAddonObservers && details->IsAddon() ? mUniversalTargets.mAddons.get() : nullptr, hasUniversalWindowObservers && details->IsWindow() ? mUniversalTargets.mWindows.get() : nullptr, group->ObservationTarget() }; bool isJankInInput = group->HasRecentUserInput(); RefPtr alert; for (nsPerformanceObservationTarget* target : targets) { if (!target) { continue; } if (!alert) { const uint32_t reason = nsIPerformanceAlert::REASON_SLOWDOWN | (isJankInAnimation ? nsIPerformanceAlert::REASON_JANK_IN_ANIMATION : 0) | (isJankInInput ? nsIPerformanceAlert::REASON_JANK_IN_INPUT : 0); // Wait until we are sure we need to allocate before we allocate. alert = new PerformanceAlert(reason, group); } target->NotifyJankObservers(details, alert); } group->ResetRecent(); } } NS_IMETHODIMP nsPerformanceStatsService::GetObservableAddon(const nsAString& addonId, nsIPerformanceObservable** result) { if (addonId.Equals(NS_LITERAL_STRING("*"))) { NS_IF_ADDREF(*result = mUniversalTargets.mAddons); } else { auto entry = mAddonIdToGroup.PutEntry(addonId); NS_IF_ADDREF(*result = entry->ObservationTarget()); } return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::GetObservableWindow(uint64_t windowId, nsIPerformanceObservable** result) { if (windowId == 0) { NS_IF_ADDREF(*result = mUniversalTargets.mWindows); } else { auto entry = mWindowIdToGroup.PutEntry(windowId); NS_IF_ADDREF(*result = entry->ObservationTarget()); } return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::GetAnimationJankLevelThreshold(short* result) { *result = mJankLevelVisibilityThreshold; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::SetAnimationJankLevelThreshold(short value) { mJankLevelVisibilityThreshold = value; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::GetUserInputDelayThreshold(uint64_t* result) { *result = mMaxExpectedDurationOfInteractionUS; return NS_OK; } NS_IMETHODIMP nsPerformanceStatsService::SetUserInputDelayThreshold(uint64_t value) { mMaxExpectedDurationOfInteractionUS = value; return NS_OK; } nsPerformanceStatsService::UniversalTargets::UniversalTargets() : mAddons(new nsPerformanceObservationTarget()) , mWindows(new nsPerformanceObservationTarget()) { } /* ------------------------------------------------------ * * Class nsPerformanceGroup * */ /*static*/ nsPerformanceGroup* nsPerformanceGroup::Make(JSContext* cx, nsPerformanceStatsService* service, const nsAString& name, const nsAString& addonId, uint64_t windowId, uint64_t processId, bool isSystem, GroupScope scope) { nsString groupId; ::GenerateUniqueGroupId(cx, service->GetNextId(), processId, groupId); return new nsPerformanceGroup(service, name, groupId, addonId, windowId, processId, isSystem, scope); } nsPerformanceGroup::nsPerformanceGroup(nsPerformanceStatsService* service, const nsAString& name, const nsAString& groupId, const nsAString& addonId, uint64_t windowId, uint64_t processId, bool isSystem, GroupScope scope) : mDetails(new nsPerformanceGroupDetails(name, groupId, addonId, windowId, processId, isSystem)) , mService(service) , mScope(scope) , mHighestJank(0) , mHighestCPOW(0) , mHasRecentUserInput(false) , mHasPendingAlert(false) { mozilla::Unused << mService->mGroups.PutEntry(this); #if defined(DEBUG) if (scope == GroupScope::ADDON) { MOZ_ASSERT(mDetails->IsAddon()); MOZ_ASSERT(!mDetails->IsWindow()); } else if (scope == GroupScope::WINDOW) { MOZ_ASSERT(mDetails->IsWindow()); MOZ_ASSERT(!mDetails->IsAddon()); } else if (scope == GroupScope::RUNTIME) { MOZ_ASSERT(!mDetails->IsWindow()); MOZ_ASSERT(!mDetails->IsAddon()); } #endif // defined(DEBUG) setIsActive(mScope != GroupScope::COMPARTMENT || mService->mIsMonitoringPerCompartment); } void nsPerformanceGroup::Dispose() { if (!mService) { // We have already called `Dispose()`. return; } if (mObservationTarget) { mObservationTarget = nullptr; } // Remove any reference to the service. RefPtr service; service.swap(mService); // Remove any dangling pointer to `this`. service->mGroups.RemoveEntry(this); if (mScope == GroupScope::ADDON) { MOZ_ASSERT(mDetails->IsAddon()); service->mAddonIdToGroup.RemoveEntry(mDetails->AddonId()); } else if (mScope == GroupScope::WINDOW) { MOZ_ASSERT(mDetails->IsWindow()); service->mWindowIdToGroup.RemoveEntry(mDetails->WindowId()); } } nsPerformanceGroup::~nsPerformanceGroup() { Dispose(); } nsPerformanceGroup::GroupScope nsPerformanceGroup::Scope() const { return mScope; } nsPerformanceGroupDetails* nsPerformanceGroup::Details() const { return mDetails; } void nsPerformanceGroup::SetObservationTarget(nsPerformanceObservationTarget* target) { MOZ_ASSERT(!mObservationTarget); mObservationTarget = target; } nsPerformanceObservationTarget* nsPerformanceGroup::ObservationTarget() const { return mObservationTarget; } bool nsPerformanceGroup::HasPendingAlert() const { return mHasPendingAlert; } void nsPerformanceGroup::SetHasPendingAlert(bool value) { mHasPendingAlert = value; } void nsPerformanceGroup::RecordJank(uint64_t jank) { if (jank > mHighestJank) { mHighestJank = jank; } } void nsPerformanceGroup::RecordCPOW(uint64_t cpow) { if (cpow > mHighestCPOW) { mHighestCPOW = cpow; } } uint64_t nsPerformanceGroup::HighestRecentJank() { return mHighestJank; } uint64_t nsPerformanceGroup::HighestRecentCPOW() { return mHighestCPOW; } bool nsPerformanceGroup::HasRecentUserInput() { return mHasRecentUserInput; } void nsPerformanceGroup::RecordUserInput() { mHasRecentUserInput = true; } void nsPerformanceGroup::ResetRecent() { mHighestJank = 0; mHighestCPOW = 0; mHasRecentUserInput = false; }