gecko-dev/toolkit/components/perfmonitoring/nsPerformanceStats.cpp
Gijs Kruitbosch 290c64c982 Bug 1309946 - remove all traces of add-on performance monitoring, r=Yoric
This removes all the code for add-on performance watching from the
perfmonitoring component. This should mean that for add-on
compartments, we no longer trigger jank or CPOW monitoring in the JS
engine. This should result in minor performance improvements. As a
result, about:performance no longer reports on add-on performance
(but still reports on web page performance).

It also removes the AddonWatchers.jsm module and the related Nightly-
only UI (disabled in the parent commit) and strings. This UI wasn't
ready for release, there wasn't sufficient data it was creating
value for users, and there was some evidence that it didn't always
correctly identify the cause of performance issues, thus potentially
leading to user confusion or annoyance. Removing it therefore seemed
the right thing to do.

MozReview-Commit-ID: LsRwuaUtq6L

--HG--
extra : rebase_source : 92d4b775a7a7cbb5793e74eea471be81be974dda
2017-03-29 11:03:47 +01:00

1562 lines
42 KiB
C++

/* -*- 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"
#include "mozilla/Telemetry.h"
#if defined(XP_WIN)
#include <processthreadsapi.h>
#include <windows.h>
#else
#include <unistd.h>
#endif // defined(XP_WIN)
#if defined(XP_MACOSX)
#include <mach/mach_init.h>
#include <mach/mach_interface.h>
#include <mach/mach_port.h>
#include <mach/mach_types.h>
#include <mach/message.h>
#include <mach/thread_info.h>
#elif defined(XP_UNIX)
#include <sys/time.h>
#include <sys/resource.h>
#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<nsPIDOMWindowOuter>
GetPrivateWindow(JSContext* cx) {
nsGlobalWindow* win = xpc::CurrentWindowOrNull(cx);
if (!win) {
return nullptr;
}
nsPIDOMWindowOuter* outer = win->AsInner()->GetOuterWindow();
if (!outer) {
return nullptr;
}
nsCOMPtr<nsPIDOMWindowOuter> top = outer->GetTop();
if (!top) {
return nullptr;
}
return top.forget();
}
bool
URLForGlobal(JSContext* cx, JS::Handle<JSObject*> global, nsAString& url) {
nsCOMPtr<nsIPrincipal> principal = nsContentUtils::ObjectPrincipal(global);
if (!principal) {
return false;
}
nsCOMPtr<nsIURI> 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<JSObject*> 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<uintptr_t>(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<nsCOMPtr<nsIPerformanceObserver>> observers;
if (!observers.appendAll(mObservers)) {
MOZ_CRASH();
}
// Now actually notify.
for (auto iter = observers.begin(), end = observers.end(); iter < end; ++iter) {
nsCOMPtr<nsIPerformanceObserver> 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;
}
uint64_t
nsPerformanceGroupDetails::WindowId() const {
return mWindowId;
}
uint64_t
nsPerformanceGroupDetails::ProcessId() const {
return mProcessId;
}
bool
nsPerformanceGroupDetails::IsSystem() const {
return mIsSystem;
}
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 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<nsPerformanceGroupDetails> 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<nsIPerformanceStats> mComponentsData;
/**
* The data for the process.
*/
nsCOMPtr<nsIPerformanceStats> mProcessData;
};
NS_IMPL_ISUPPORTS(nsPerformanceSnapshot, nsIPerformanceSnapshot)
/* nsIArray getComponentsData (); */
NS_IMETHODIMP
nsPerformanceSnapshot::GetComponentsData(nsIArray * *aComponents)
{
const size_t length = mComponentsData.Length();
nsCOMPtr<nsIMutableArray> components = do_CreateInstance(NS_ARRAY_CONTRACTID);
for (size_t i = 0; i < length; ++i) {
nsCOMPtr<nsIPerformanceStats> stats = mComponentsData[i];
mozilla::DebugOnly<nsresult> 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 nsINamed
{
public:
NS_DECL_ISUPPORTS
NS_DECL_NSITIMERCALLBACK
NS_DECL_NSINAMED
explicit PendingAlertsCollector(nsPerformanceStatsService* service)
: mService(service)
, mPending(false)
{ }
nsresult Start(uint32_t timerDelayMS);
nsresult Dispose();
private:
~PendingAlertsCollector() {}
RefPtr<nsPerformanceStatsService> mService;
bool mPending;
nsCOMPtr<nsITimer> mTimer;
mozilla::Vector<uint64_t> mJankLevels;
};
NS_IMPL_ISUPPORTS(PendingAlertsCollector, nsITimerCallback, nsINamed);
NS_IMETHODIMP
PendingAlertsCollector::Notify(nsITimer*) {
mPending = false;
mService->NotifyJankObservers(mJankLevels);
return NS_OK;
}
NS_IMETHODIMP
PendingAlertsCollector::GetName(nsACString& aName)
{
aName.AssignASCII("PendingAlertsCollector_timer");
return NS_OK;
}
NS_IMETHODIMP
PendingAlertsCollector::SetName(const char* aName)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
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<bool> 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("<process>"), // name
0, // windowId
mProcessId,
true, // isSystem
nsPerformanceGroup::GroupScope::RUNTIME // scope
))
, mIsHandlingUserInput(false)
, mProcessStayed(0)
, mProcessMoved(0)
, mProcessUpdateCounter(0)
, mIsMonitoringPerCompartment(false)
, mJankAlertThreshold(mozilla::MaxValue<uint64_t>::value) // By default, no alerts
, mJankAlertBufferingDelay(1000 /* ms */)
, mJankLevelVisibilityThreshold(/* 2 ^ */ 8 /* ms */)
, mMaxExpectedDurationOfInteractionUS(150 * 1000)
{
mPendingAlertsCollector = new PendingAlertsCollector(this);
nsString groupIdForWindows;
GenerateUniqueGroupId(mContext, GetNextId(), mProcessId, groupIdForWindows);
mUniversalTargets.mWindows->
SetTarget(new nsPerformanceGroupDetails(NS_LITERAL_STRING("<universal window listener>"),
groupIdForWindows,
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<nsPerformanceStatsService> kungFuDeathGrip(this);
mIsAvailable = false;
if (mDisposed) {
// Make sure that we don't double-dispose.
return;
}
mDisposed = true;
// Disconnect from nsIObserverService.
nsCOMPtr<nsIObserverService> 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.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<nsPerformanceGroup> 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<nsIObserverService> 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<nsPerformanceGroup> 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;
}
nsresult
nsPerformanceStatsService::UpdateTelemetry()
{
// Promote everything to floating-point explicitly before dividing.
const double processStayed = mProcessStayed;
const double processMoved = mProcessMoved;
if (processStayed <= 0 || processMoved <= 0 || processStayed + processMoved <= 0) {
// Overflow/underflow/nothing to report
return NS_OK;
}
const double proportion = (100 * processStayed) / (processStayed + processMoved);
if (proportion < 0 || proportion > 100) {
// Overflow/underflow
return NS_OK;
}
mozilla::Telemetry::Accumulate(mozilla::Telemetry::PERF_MONITORING_TEST_CPU_RESCHEDULING_PROPORTION_MOVED, (uint32_t)proportion);
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<nsPerformanceSnapshot> 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));
}
}
js::GetPerfMonitoringTestCpuRescheduling(cx, &mProcessStayed, &mProcessMoved);
if (++mProcessUpdateCounter % 10 == 0) {
mozilla::Unused << UpdateTelemetry();
}
snapshot.forget(aSnapshot);
return NS_OK;
}
uint64_t
nsPerformanceStatsService::GetNextId() {
return ++mUIdCounter;
}
/* static*/ bool
nsPerformanceStatsService::GetPerformanceGroupsCallback(JSContext* cx,
js::PerformanceGroupVector& out,
void* closure)
{
RefPtr<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(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 a window. If so, its
// duration should count towards the total duration of the window.
uint64_t windowId = 0;
if (nsCOMPtr<nsPIDOMWindowOuter> 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, 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, 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<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(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<nsPerformanceStatsService> self = reinterpret_cast<nsPerformanceStatsService*>(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<nsPerformanceGroup> 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_MACOSX)
// On MacOS X, to get we per-thread data, we need to
// reach into the kernel.
mach_msg_type_number_t count = THREAD_BASIC_INFO_COUNT;
thread_basic_info_data_t info;
mach_port_t port = mach_thread_self();
kern_return_t err =
thread_info(/* [in] targeted thread*/ port,
/* [in] nature of information*/ THREAD_BASIC_INFO,
/* [out] thread information */ (thread_info_t)&info,
/* [inout] number of items */ &count);
// We do not need ability to communicate with the thread, so
// let's release the port.
mach_port_deallocate(mach_task_self(), port);
if (err != KERN_SUCCESS)
return NS_ERROR_FAILURE;
*userTime = info.user_time.microseconds + info.user_time.seconds * 1000000;
*systemTime = info.system_time.microseconds + info.system_time.seconds * 1000000;
#elif 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_MACOSX) || defined(XP_UNIX) || defined(XP_WIN)
return NS_OK;
}
void
nsPerformanceStatsService::NotifyJankObservers(const mozilla::Vector<uint64_t>& 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<uint64_t> latestJankLevels;
{
mozilla::DebugOnly<bool> 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 hasUniversalWindowObservers = mUniversalTargets.mWindows->HasObservers();
for (auto iter = alerts.begin(); iter < alerts.end(); ++iter) {
MOZ_ASSERT(iter);
RefPtr<nsPerformanceGroup> group = *iter;
group->SetHasPendingAlert(false);
RefPtr<nsPerformanceGroupDetails> details = group->Details();
nsPerformanceObservationTarget* targets[3] = {
hasUniversalWindowObservers && details->IsWindow() ? mUniversalTargets.mWindows.get() : nullptr,
group->ObservationTarget()
};
bool isJankInInput = group->HasRecentUserInput();
RefPtr<PerformanceAlert> 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::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()
: mWindows(new nsPerformanceObservationTarget())
{ }
/* ------------------------------------------------------
*
* Class nsPerformanceGroup
*
*/
/*static*/ nsPerformanceGroup*
nsPerformanceGroup::Make(JSContext* cx,
nsPerformanceStatsService* service,
const nsAString& name,
uint64_t windowId,
uint64_t processId,
bool isSystem,
GroupScope scope)
{
nsString groupId;
::GenerateUniqueGroupId(cx, service->GetNextId(), processId, groupId);
return new nsPerformanceGroup(service, name, groupId, windowId, processId, isSystem, scope);
}
nsPerformanceGroup::nsPerformanceGroup(nsPerformanceStatsService* service,
const nsAString& name,
const nsAString& groupId,
uint64_t windowId,
uint64_t processId,
bool isSystem,
GroupScope scope)
: mDetails(new nsPerformanceGroupDetails(name, groupId, 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::WINDOW) {
MOZ_ASSERT(mDetails->IsWindow());
} else if (scope == GroupScope::RUNTIME) {
MOZ_ASSERT(!mDetails->IsWindow());
}
#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<nsPerformanceStatsService> service;
service.swap(mService);
// Remove any dangling pointer to `this`.
service->mGroups.RemoveEntry(this);
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;
}