gecko-dev/toolkit/components/perfmonitoring/nsPerformanceStats.h

826 lines
24 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/. */
#ifndef nsPerformanceStats_h
#define nsPerformanceStats_h
#include "jsapi.h"
#include "nsHashKeys.h"
#include "nsTHashtable.h"
#include "nsIObserver.h"
#include "nsPIDOMWindow.h"
#include "nsIPerformanceStats.h"
class nsPerformanceGroup;
class nsPerformanceGroupDetails;
typedef mozilla::Vector<RefPtr<nsPerformanceGroup>> GroupVector;
/**
* A data structure for registering observers interested in
* performance alerts.
*
* Each performance group owns a single instance of this class.
* Additionally, the service owns instances designed to observe the
* performance alerts in all add-ons (respectively webpages).
*/
class nsPerformanceObservationTarget final: public nsIPerformanceObservable {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIPERFORMANCEOBSERVABLE
/**
* `true` if this target has at least once performance observer
* registered, `false` otherwise.
*/
bool HasObservers() const;
/**
* Notify all the observers that jank has happened.
*/
void NotifyJankObservers(nsIPerformanceGroupDetails* source, nsIPerformanceAlert* gravity);
/**
* Set the details on the group being observed.
*/
void SetTarget(nsPerformanceGroupDetails* details);
private:
~nsPerformanceObservationTarget() {}
// The observers for this target. We hold them as a vector, despite
// the linear removal cost, as we expect that the typical number of
// observers will be lower than 3, and that (un)registrations will
// be fairly infrequent.
mozilla::Vector<nsCOMPtr<nsIPerformanceObserver>> mObservers;
// Details on the group being observed. May be `nullptr`.
RefPtr<nsPerformanceGroupDetails> mDetails;
};
/**
* The base class for entries of maps from addon id/window id to
* performance group.
*
* Performance observers may be registered before their group is
* created (e.g., one may register an observer for an add-on before
* all its modules are loaded, or even before the add-on is loaded at
* all or for an observer for a webpage before all its iframes are
* loaded). This class serves to hold the observation target until the
* performance group may be created, and then to associate the
* observation target and the performance group.
*/
class nsGroupHolder {
public:
nsGroupHolder()
: mGroup(nullptr)
, mPendingObservationTarget(nullptr)
{ }
/**
* Get the observation target, creating it if necessary.
*/
nsPerformanceObservationTarget* ObservationTarget();
/**
* Get the group, if it has been created.
*
* May return `null` if the group hasn't been created yet.
*/
class nsPerformanceGroup* GetGroup();
/**
* Set the group.
*
* Once this method has been called, calling
* `this->ObservationTarget()` and `group->ObservationTarget()` is equivalent.
*
* Must only be called once.
*/
void SetGroup(class nsPerformanceGroup*);
private:
// The group. Initially `nullptr`, until we have called `SetGroup`.
class nsPerformanceGroup* mGroup;
// The observation target. Instantiated by the first call to
// `ObservationTarget()`.
RefPtr<nsPerformanceObservationTarget> mPendingObservationTarget;
};
/**
* An implementation of the nsIPerformanceStatsService.
*
* Note that this implementation is not thread-safe.
*/
class nsPerformanceStatsService final : public nsIPerformanceStatsService,
public nsIObserver
{
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIPERFORMANCESTATSSERVICE
NS_DECL_NSIOBSERVER
nsPerformanceStatsService();
nsresult Init();
private:
nsresult InitInternal();
void Dispose();
~nsPerformanceStatsService();
protected:
friend nsPerformanceGroup;
/**
* `false` until `Init()` and after `Dispose()`, `true` inbetween.
*/
bool mIsAvailable;
/**
* `true` once we have called `Dispose()`.
*/
bool mDisposed;
/**
* A unique identifier for the process.
*
* Process HANDLE under Windows, pid under Unix.
*/
const uint64_t mProcessId;
/**
* The JS Runtime for the main thread.
*/
JSRuntime* const mRuntime;
/**
* Generate unique identifiers.
*/
uint64_t GetNextId();
uint64_t mUIdCounter;
/**
* Extract a snapshot of performance statistics from a performance group.
*/
static nsIPerformanceStats* GetStatsForGroup(const js::PerformanceGroup* group);
static nsIPerformanceStats* GetStatsForGroup(const nsPerformanceGroup* group);
/**
* Get the performance groups associated to a given JS compartment.
*
* A compartment is typically associated to the following groups:
* - the top group, shared by the entire process;
* - the window group, if the code is executed in a window, shared
* by all compartments for that window (typically, all frames);
* - the add-on group, if the code is executed as an add-on, shared
* by all compartments for that add-on (typically, all modules);
* - the compartment's own group.
*
* Pre-condition: the VM must have entered the JS compartment.
*
* The caller is expected to cache the results of this method, as
* calling it more than once may not return the same instances of
* performance groups.
*/
bool GetPerformanceGroups(JSContext* cx, js::PerformanceGroupVector&);
static bool GetPerformanceGroupsCallback(JSContext* cx, js::PerformanceGroupVector&, void* closure);
/**********************************************************
*
* Sets of all performance groups, indexed by several keys.
*
* These sets do not keep the performance groups alive. Rather, a
* performance group is inserted in the relevant sets upon
* construction and removed from the sets upon destruction or when
* we Dispose() of the service.
*
* A `nsPerformanceGroup` is typically kept alive (as a
* `js::PerformanceGroup`) by the JSCompartment to which it is
* associated. It may also temporarily be kept alive by the JS
* stack, in particular in case of nested event loops.
*/
/**
* Set of performance groups associated to add-ons, indexed
* by add-on id. Each item is shared by all the compartments
* that belong to the add-on.
*/
struct AddonIdToGroup: public nsStringHashKey,
public nsGroupHolder {
explicit AddonIdToGroup(const nsAString* key)
: nsStringHashKey(key)
{ }
};
nsTHashtable<AddonIdToGroup> mAddonIdToGroup;
/**
* Set of performance groups associated to windows, indexed by outer
* window id. Each item is shared by all the compartments that
* belong to the window.
*/
struct WindowIdToGroup: public nsUint64HashKey,
public nsGroupHolder {
explicit WindowIdToGroup(const uint64_t* key)
: nsUint64HashKey(key)
{}
};
nsTHashtable<WindowIdToGroup> mWindowIdToGroup;
/**
* Set of all performance groups.
*/
struct Groups: public nsPtrHashKey<nsPerformanceGroup> {
explicit Groups(const nsPerformanceGroup* key)
: nsPtrHashKey<nsPerformanceGroup>(key)
{}
};
nsTHashtable<Groups> mGroups;
/**
* The performance group representing the runtime itself. All
* compartments are associated to this group.
*/
RefPtr<nsPerformanceGroup> mTopGroup;
/**********************************************************
*
* Measuring and recording the CPU use of the system.
*
*/
/**
* Get the OS-reported time spent in userland/systemland, in
* microseconds. On most platforms, this data is per-thread,
* but on some platforms we need to fall back to per-process.
*
* Data is not guaranteed to be monotonic.
*/
nsresult GetResources(uint64_t* userTime, uint64_t* systemTime) const;
/**
* Amount of user/system CPU time used by the thread (or process,
* for platforms that don't support per-thread measure) since start.
* Updated by `StopwatchStart` at most once per event.
*
* Unit: microseconds.
*/
uint64_t mUserTimeStart;
uint64_t mSystemTimeStart;
bool mIsHandlingUserInput;
/**
* The number of user inputs since the start of the process. Used to
* determine whether the current iteration has triggered a
* (JS-implemented) user input.
*/
uint64_t mUserInputCount;
/**********************************************************
*
* Callbacks triggered by the JS VM when execution of JavaScript
* code starts/completes.
*
* As measures of user CPU time/system CPU time have low resolution
* (and are somewhat slow), we measure both only during the calls to
* `StopwatchStart`/`StopwatchCommit` and we make the assumption
* that each group's user/system CPU time is proportional to the
* number of clock cycles spent executing code in the group between
* `StopwatchStart`/`StopwatchCommit`.
*
* The results may be skewed by the thread being rescheduled to a
* different CPU during the measure, but we expect that on average,
* the skew will have limited effects, and will generally tend to
* make already-slow executions appear slower.
*/
/**
* Execution of JavaScript code has started. This may happen several
* times in succession if the JavaScript code contains nested event
* loops, in which case only the innermost call will receive
* `StopwatchCommitCallback`.
*
* @param iteration The number of times we have started executing
* JavaScript code.
*/
static bool StopwatchStartCallback(uint64_t iteration, void* closure);
bool StopwatchStart(uint64_t iteration);
/**
* Execution of JavaScript code has reached completion (including
* enqueued microtasks). In cse of tested event loops, any ongoing
* measurement on outer loops is silently cancelled without any call
* to this method.
*
* @param iteration The number of times we have started executing
* JavaScript code.
* @param recentGroups The groups that have seen activity during this
* event.
*/
static bool StopwatchCommitCallback(uint64_t iteration,
js::PerformanceGroupVector& recentGroups,
void* closure);
bool StopwatchCommit(uint64_t iteration, js::PerformanceGroupVector& recentGroups);
/**
* The number of times we have started executing JavaScript code.
*/
uint64_t mIteration;
/**
* Commit performance measures of a single group.
*
* Data is transfered from `group->recent*` to `group->data`.
*
*
* @param iteration The current iteration.
* @param userTime The total user CPU time for this thread (or
* process, if per-thread data is not available) between the
* calls to `StopwatchStart` and `StopwatchCommit`.
* @param systemTime The total system CPU time for this thread (or
* process, if per-thread data is not available) between the
* calls to `StopwatchStart` and `StopwatchCommit`.
* @param cycles The total number of cycles for this thread
* between the calls to `StopwatchStart` and `StopwatchCommit`.
* @param isJankVisible If `true`, expect that the user will notice
* any slowdown.
* @param group The group containing the data to commit.
*/
void CommitGroup(uint64_t iteration,
uint64_t userTime, uint64_t systemTime, uint64_t cycles,
bool isJankVisible,
nsPerformanceGroup* group);
/**********************************************************
*
* To check whether our algorithm makes sense, we keep count of the
* number of times the process has been rescheduled to another CPU
* while we were monitoring the performance of a group and we upload
* this data through Telemetry.
*/
nsresult UpdateTelemetry();
uint64_t mProcessStayed;
uint64_t mProcessMoved;
uint32_t mProcessUpdateCounter;
/**********************************************************
*
* Options controlling measurements.
*/
/**
* Determine if we are measuring the performance of every individual
* compartment (in particular, every individual module, frame,
* sandbox). Note that this makes measurements noticeably slower.
*/
bool mIsMonitoringPerCompartment;
/**********************************************************
*
* Determining whether jank is user-visible.
*/
/**
* `true` if we believe that any slowdown can cause a noticeable
* delay in handling user-input.
*
* In the current implementation, we return `true` if the latest
* user input was less than MAX_DURATION_OF_INTERACTION_MS ago. This
* includes all inputs (mouse, keyboard, other devices), with the
* exception of mousemove.
*/
bool IsHandlingUserInput();
public:
/**********************************************************
*
* Letting observers register themselves to watch for performance
* alerts.
*
* To avoid saturating clients with alerts (or even creating loops
* of alerts), each alert is buffered. At the end of each iteration
* of the event loop, groups that have caused performance alerts
* are registered in a set of pending alerts, and the collection
* timer hasn't been started yet, it is started. Once the timer
* firers, we gather all the pending alerts, empty the set and
* dispatch to observers.
*/
/**
* Clear the set of pending alerts and dispatch the pending alerts
* to observers.
*/
void NotifyJankObservers(const mozilla::Vector<uint64_t>& previousJankLevels);
private:
/**
* The set of groups for which we know that an alert should be
* raised. This set is cleared once `mPendingAlertsCollector`
* fires.
*
* Invariant: no group may appear twice in this vector.
*/
GroupVector mPendingAlerts;
/**
* A timer callback in charge of collecting the groups in
* `mPendingAlerts` and triggering `NotifyJankObservers` to dispatch
* performance alerts.
*/
RefPtr<class PendingAlertsCollector> mPendingAlertsCollector;
/**
* Observation targets that are not attached to a specific group.
*/
struct UniversalTargets {
UniversalTargets();
/**
* A target for observers interested in watching all addons.
*/
RefPtr<nsPerformanceObservationTarget> mAddons;
/**
* A target for observers interested in watching all windows.
*/
RefPtr<nsPerformanceObservationTarget> mWindows;
};
UniversalTargets mUniversalTargets;
/**
* The threshold, in microseconds, above which a performance group is
* considered "slow" and should raise performance alerts.
*/
uint64_t mJankAlertThreshold;
/**
* A buffering delay, in milliseconds, used by the service to
* regroup performance alerts, before observers are actually
* noticed. Higher delays let the system avoid redundant
* notifications for the same group, and are generally better for
* performance.
*/
uint32_t mJankAlertBufferingDelay;
/**
* The threshold above which jank, as reported by the refresh drivers,
* is considered user-visible.
*
* A value of n means that any jank above 2^n ms will be considered
* user visible.
*/
short mJankLevelVisibilityThreshold;
/**
* The number of microseconds during which we assume that a
* user-interaction can keep the code jank-critical. Any user
* interaction that lasts longer than this duration is expected to
* either have already caused jank or have caused a nested event
* loop.
*
* In either case, we consider that monitoring
* jank-during-interaction after this duration is useless.
*/
uint64_t mMaxExpectedDurationOfInteractionUS;
};
/**
* Container for performance data.
*
* All values are monotonic.
*
* All values are updated after running to completion.
*/
struct PerformanceData {
/**
* Number of times we have spent at least 2^n consecutive
* milliseconds executing code in this group.
* durations[0] is increased whenever we spend at least 1 ms
* executing code in this group
* durations[1] whenever we spend 2ms+
* ...
* durations[i] whenever we spend 2^ims+
*/
uint64_t mDurations[10];
/**
* Total amount of time spent executing code in this group, in
* microseconds.
*/
uint64_t mTotalUserTime;
uint64_t mTotalSystemTime;
uint64_t mTotalCPOWTime;
/**
* Total number of times code execution entered this group, since
* process launch. This may be greater than the number of times we
* have entered the event loop.
*/
uint64_t mTicks;
PerformanceData();
PerformanceData(const PerformanceData& from) = default;
PerformanceData& operator=(const PerformanceData& from) = default;
};
/**
* Identification information for an item that can hold performance
* data.
*/
class nsPerformanceGroupDetails final: public nsIPerformanceGroupDetails {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIPERFORMANCEGROUPDETAILS
nsPerformanceGroupDetails(const nsAString& aName,
const nsAString& aGroupId,
const nsAString& aAddonId,
const uint64_t aWindowId,
const uint64_t aProcessId,
const bool aIsSystem)
: mName(aName)
, mGroupId(aGroupId)
, mAddonId(aAddonId)
, mWindowId(aWindowId)
, mProcessId(aProcessId)
, mIsSystem(aIsSystem)
{ }
public:
const nsAString& Name() const;
const nsAString& GroupId() const;
const nsAString& AddonId() const;
uint64_t WindowId() const;
uint64_t ProcessId() const;
bool IsAddon() const;
bool IsWindow() const;
bool IsSystem() const;
bool IsContentProcess() const;
private:
~nsPerformanceGroupDetails() {}
const nsString mName;
const nsString mGroupId;
const nsString mAddonId;
const uint64_t mWindowId;
const uint64_t mProcessId;
const bool mIsSystem;
};
/**
* The kind of compartments represented by this group.
*/
enum class PerformanceGroupScope {
/**
* This group represents the entire runtime (i.e. the thread).
*/
RUNTIME,
/**
* This group represents all the compartments executed in a window.
*/
WINDOW,
/**
* This group represents all the compartments provided by an addon.
*/
ADDON,
/**
* This group represents a single compartment.
*/
COMPARTMENT,
};
/**
* A concrete implementation of `js::PerformanceGroup`, also holding
* performance data. Instances may represent individual compartments,
* windows, addons or the entire runtime.
*
* This class is intended to be the sole implementation of
* `js::PerformanceGroup`.
*/
class nsPerformanceGroup final: public js::PerformanceGroup {
public:
// Ideally, we would define the enum class in nsPerformanceGroup,
// but this seems to choke some versions of gcc.
typedef PerformanceGroupScope GroupScope;
/**
* Construct a performance group.
*
* @param rt The container runtime. Used to generate a unique identifier.
* @param service The performance service. Used during destruction to
* cleanup the hash tables.
* @param name A name for the group, designed mostly for debugging purposes,
* so it should be at least somewhat human-readable.
* @param addonId The identifier of the add-on. Should be "" when the
* group is not part of an add-on,
* @param windowId The identifier of the window. Should be 0 when the
* group is not part of a window.
* @param processId A unique identifier for the process.
* @param isSystem `true` if the code of the group is executed with
* system credentials, `false` otherwise.
* @param scope the scope of this group.
*/
static nsPerformanceGroup*
Make(JSRuntime* rt,
nsPerformanceStatsService* service,
const nsAString& name,
const nsAString& addonId,
uint64_t windowId,
uint64_t processId,
bool isSystem,
GroupScope scope);
/**
* Utility: type-safer conversion from js::PerformanceGroup to nsPerformanceGroup.
*/
static inline nsPerformanceGroup* Get(js::PerformanceGroup* self) {
return static_cast<nsPerformanceGroup*>(self);
}
static inline const nsPerformanceGroup* Get(const js::PerformanceGroup* self) {
return static_cast<const nsPerformanceGroup*>(self);
}
/**
* The performance data committed to this group.
*/
PerformanceData data;
/**
* The scope of this group. Used to determine whether the group
* should be (de)activated.
*/
GroupScope Scope() const;
/**
* Identification details for this group.
*/
nsPerformanceGroupDetails* Details() const;
/**
* Cleanup any references.
*/
void Dispose();
/**
* Set the observation target for this group.
*
* This method must be called exactly once, when the performance
* group is attached to its `nsGroupHolder`.
*/
void SetObservationTarget(nsPerformanceObservationTarget*);
/**
* `true` if we have already noticed that a performance alert should
* be raised for this group but we have not dispatched it yet,
* `false` otherwise.
*/
bool HasPendingAlert() const;
void SetHasPendingAlert(bool value);
protected:
nsPerformanceGroup(nsPerformanceStatsService* service,
const nsAString& name,
const nsAString& groupId,
const nsAString& addonId,
uint64_t windowId,
uint64_t processId,
bool isSystem,
GroupScope scope);
/**
* Virtual implementation of `delete`, to make sure that objects are
* destoyed with an implementation of `delete` compatible with the
* implementation of `new` used to allocate them.
*
* Called by SpiderMonkey.
*/
virtual void Delete() override {
delete this;
}
~nsPerformanceGroup();
private:
/**
* Identification details for this group.
*/
RefPtr<nsPerformanceGroupDetails> mDetails;
/**
* The stats service. Used to perform cleanup during destruction.
*/
RefPtr<nsPerformanceStatsService> mService;
/**
* The scope of this group. Used to determine whether the group
* should be (de)activated.
*/
const GroupScope mScope;
// Observing performance alerts.
public:
/**
* The observation target, used to register observers.
*/
nsPerformanceObservationTarget* ObservationTarget() const;
/**
* Record a jank duration.
*
* Update the highest recent jank if necessary.
*/
void RecordJank(uint64_t jank);
uint64_t HighestRecentJank();
/**
* Record a CPOW duration.
*
* Update the highest recent CPOW if necessary.
*/
void RecordCPOW(uint64_t cpow);
uint64_t HighestRecentCPOW();
/**
* Record that this group has recently been involved in handling
* user input. Note that heuristics are involved here, so the
* result is not 100% accurate.
*/
void RecordUserInput();
bool HasRecentUserInput();
/**
* Reset recent values (recent highest CPOW and jank, involvement in
* user input).
*/
void ResetRecent();
private:
/**
* The target used by observers to register for watching slow
* performance alerts caused by this group.
*
* May be nullptr for groups that cannot be watched (the top group).
*/
RefPtr<class nsPerformanceObservationTarget> mObservationTarget;
/**
* The highest jank encountered since jank observers for this group
* were last called, in microseconds.
*/
uint64_t mHighestJank;
/**
* The highest CPOW encountered since jank observers for this group
* were last called, in microseconds.
*/
uint64_t mHighestCPOW;
/**
* `true` if this group has been involved in handling user input,
* `false` otherwise.
*
* Note that we use heuristics to determine whether a group is
* involved in handling user input, so this value is not 100%
* accurate.
*/
bool mHasRecentUserInput;
/**
* `true` if this group has caused a performance alert and this alert
* hasn't been dispatched yet.
*
* We use this as part of the buffering of performance alerts. If
* the group generates several alerts several times during the
* buffering delay, we only wish to add the group once to the list
* of alerts.
*/
bool mHasPendingAlert;
};
#endif