summaryrefslogtreecommitdiffstats
path: root/toolkit/components/processtools
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/processtools')
-rw-r--r--toolkit/components/processtools/Cargo.toml15
-rw-r--r--toolkit/components/processtools/ProcInfo.h279
-rw-r--r--toolkit/components/processtools/ProcInfo.mm186
-rw-r--r--toolkit/components/processtools/ProcInfo_bsd.cpp115
-rw-r--r--toolkit/components/processtools/ProcInfo_common.cpp67
-rw-r--r--toolkit/components/processtools/ProcInfo_linux.cpp344
-rw-r--r--toolkit/components/processtools/ProcInfo_linux.h26
-rw-r--r--toolkit/components/processtools/ProcInfo_solaris.cpp124
-rw-r--r--toolkit/components/processtools/ProcInfo_win.cpp295
-rw-r--r--toolkit/components/processtools/ProcessToolsService.cpp33
-rw-r--r--toolkit/components/processtools/ProcessToolsService.h8
-rw-r--r--toolkit/components/processtools/components.conf10
-rw-r--r--toolkit/components/processtools/metrics.yaml337
-rw-r--r--toolkit/components/processtools/moz.build53
-rw-r--r--toolkit/components/processtools/nsIProcessToolsService.idl48
-rw-r--r--toolkit/components/processtools/src/lib.rs197
-rw-r--r--toolkit/components/processtools/tests/browser/browser.toml11
-rw-r--r--toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js417
-rw-r--r--toolkit/components/processtools/tests/browser/browser_test_procinfo.js166
-rw-r--r--toolkit/components/processtools/tests/browser/dummy.html19
-rw-r--r--toolkit/components/processtools/tests/xpcshell/test_process_kill.js52
-rw-r--r--toolkit/components/processtools/tests/xpcshell/test_total_cpu_time.js122
-rw-r--r--toolkit/components/processtools/tests/xpcshell/test_total_cpu_time_child.js10
-rw-r--r--toolkit/components/processtools/tests/xpcshell/xpcshell.toml10
24 files changed, 2944 insertions, 0 deletions
diff --git a/toolkit/components/processtools/Cargo.toml b/toolkit/components/processtools/Cargo.toml
new file mode 100644
index 0000000000..bd56de8e63
--- /dev/null
+++ b/toolkit/components/processtools/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "processtools"
+version = "0.1.0"
+authors = ["David Teller <dteller@mozilla.com>"]
+license = "MPL-2.0"
+
+[dependencies]
+nserror = { path = "../../../xpcom/rust/nserror" }
+xpcom = { path = "../../../xpcom/rust/xpcom" }
+
+[target.'cfg(windows)'.dependencies]
+winapi = "0.3.7"
+
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
diff --git a/toolkit/components/processtools/ProcInfo.h b/toolkit/components/processtools/ProcInfo.h
new file mode 100644
index 0000000000..d7e557b42e
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo.h
@@ -0,0 +1,279 @@
+/* -*- 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 __mozilla_ProcInfo_h
+#define __mozilla_ProcInfo_h
+
+#include <base/process.h>
+#include <stdint.h>
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/ChromeUtilsBinding.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/HashTable.h"
+#include "mozilla/MozPromise.h"
+
+namespace mozilla {
+
+namespace ipc {
+class GeckoChildProcessHost;
+}
+
+/**
+ * Return the number of milliseconds of CPU time used since process start.
+ *
+ * @return NS_OK on success.
+ */
+nsresult GetCpuTimeSinceProcessStartInMs(uint64_t* aResult);
+
+/**
+ * Return the number of milliseconds of GPU time used since process start.
+ *
+ * @return NS_OK on success.
+ */
+nsresult GetGpuTimeSinceProcessStartInMs(uint64_t* aResult);
+
+// Process types. When updating this enum, please make sure to update
+// WebIDLProcType, ChromeUtils::RequestProcInfo and ProcTypeToWebIDL to
+// mirror the changes.
+enum class ProcType {
+ // These must match the ones in RemoteType.h, and E10SUtils.sys.mjs
+ Web,
+ WebIsolated,
+ File,
+ Extension,
+ PrivilegedAbout,
+ PrivilegedMozilla,
+ WebCOOPCOEP,
+ WebServiceWorker,
+// the rest matches GeckoProcessTypes.h
+#define GECKO_PROCESS_TYPE(enum_value, enum_name, string_name, proc_typename, \
+ process_bin_type, procinfo_typename, \
+ webidl_typename, allcaps_name) \
+ procinfo_typename,
+#define SKIP_PROCESS_TYPE_CONTENT
+#ifndef MOZ_ENABLE_FORKSERVER
+# define SKIP_PROCESS_TYPE_FORKSERVER
+#endif // MOZ_ENABLE_FORKSERVER
+#include "mozilla/GeckoProcessTypes.h"
+#undef SKIP_PROCESS_TYPE_CONTENT
+#ifndef MOZ_ENABLE_FORKSERVER
+# undef SKIP_PROCESS_TYPE_FORKSERVER
+#endif // MOZ_ENABLE_FORKSERVER
+#undef GECKO_PROCESS_TYPE
+ Preallocated,
+ // Unknown type of process
+ Unknown,
+ Max = Unknown,
+};
+
+using UtilityActorName = mozilla::dom::WebIDLUtilityActorName;
+
+// String that will be used e.g. to annotate crash reports
+nsCString GetUtilityActorName(const UtilityActorName aActorName);
+
+#ifdef XP_WIN
+int GetCpuFrequencyMHz();
+#endif
+
+/* Get the CPU frequency to use to convert cycle time values to actual time.
+ * @returns the TSC (Time Stamp Counter) frequency in MHz, or 0 if converting
+ * cycle time values should not be attempted. */
+int GetCycleTimeFrequencyMHz();
+
+struct ThreadInfo {
+ // Thread Id.
+ base::ProcessId tid = 0;
+ // Thread name, if any.
+ nsString name;
+ // CPU time in ns.
+ uint64_t cpuTime = 0;
+ // CPU time in cycles if available.
+ uint64_t cpuCycleCount = 0;
+};
+
+// Info on a DOM window.
+struct WindowInfo {
+ explicit WindowInfo()
+ : outerWindowId(0),
+ documentURI(nullptr),
+ documentTitle(u""_ns),
+ isProcessRoot(false),
+ isInProcess(false) {}
+ WindowInfo(uint64_t aOuterWindowId, nsIURI* aDocumentURI,
+ nsAString&& aDocumentTitle, bool aIsProcessRoot, bool aIsInProcess)
+ : outerWindowId(aOuterWindowId),
+ documentURI(aDocumentURI),
+ documentTitle(std::move(aDocumentTitle)),
+ isProcessRoot(aIsProcessRoot),
+ isInProcess(aIsInProcess) {}
+
+ // Internal window id.
+ const uint64_t outerWindowId;
+
+ // URI of the document.
+ const nsCOMPtr<nsIURI> documentURI;
+
+ // Title of the document.
+ const nsString documentTitle;
+
+ // True if this is the toplevel window of the process.
+ // Note that this may be an iframe from another process.
+ const bool isProcessRoot;
+
+ const bool isInProcess;
+};
+
+// Info on a Utility process actor
+struct UtilityInfo {
+ explicit UtilityInfo() : actorName(UtilityActorName::Unknown) {}
+ explicit UtilityInfo(UtilityActorName aActorName) : actorName(aActorName) {}
+ const UtilityActorName actorName;
+};
+
+struct ProcInfo {
+ // Process Id
+ base::ProcessId pid = 0;
+ // Child Id as defined by Firefox when a child process is created.
+ dom::ContentParentId childId;
+ // Process type
+ ProcType type;
+ // Origin, if any
+ nsCString origin;
+ // Memory size in bytes.
+ uint64_t memory = 0;
+ // CPU time in ns.
+ uint64_t cpuTime = 0;
+ uint64_t cpuCycleCount = 0;
+ // Threads owned by this process.
+ CopyableTArray<ThreadInfo> threads;
+ // DOM windows represented by this process.
+ CopyableTArray<WindowInfo> windows;
+ // Utility process actors, empty for non Utility process
+ CopyableTArray<UtilityInfo> utilityActors;
+};
+
+typedef MozPromise<mozilla::HashMap<base::ProcessId, ProcInfo>, nsresult, true>
+ ProcInfoPromise;
+
+/**
+ * Data we need to request process info (e.g. CPU usage, memory usage)
+ * from the operating system and populate the resulting `ProcInfo`.
+ *
+ * Note that this structure contains a mix of:
+ * - low-level handles that we need to request low-level process info
+ * (`aChildTask` on macOS, `aPid` on other platforms); and
+ * - high-level data that we already acquired while looking for
+ * `aPid`/`aChildTask` and that we will need further down the road.
+ */
+struct ProcInfoRequest {
+ ProcInfoRequest(base::ProcessId aPid, ProcType aProcessType,
+ const nsACString& aOrigin, nsTArray<WindowInfo>&& aWindowInfo,
+ nsTArray<UtilityInfo>&& aUtilityInfo, uint32_t aChildId = 0
+#ifdef XP_MACOSX
+ ,
+ mach_port_t aChildTask = 0
+#endif // XP_MACOSX
+ )
+ : pid(aPid),
+ processType(aProcessType),
+ origin(aOrigin),
+ windowInfo(std::move(aWindowInfo)),
+ utilityInfo(std::move(aUtilityInfo)),
+ childId(aChildId)
+#ifdef XP_MACOSX
+ ,
+ childTask(aChildTask)
+#endif // XP_MACOSX
+ {
+ }
+ const base::ProcessId pid;
+ const ProcType processType;
+ const nsCString origin;
+ const nsTArray<WindowInfo> windowInfo;
+ const nsTArray<UtilityInfo> utilityInfo;
+ // If the process is a child, its child id, otherwise `0`.
+ const int32_t childId;
+#ifdef XP_MACOSX
+ const mach_port_t childTask;
+#endif // XP_MACOSX
+};
+
+/**
+ * Batch a request for low-level information on Gecko processes.
+ *
+ * # Request
+ *
+ * Argument `aRequests` is a list of processes, along with high-level data
+ * we have already obtained on them and that we need to populate the
+ * resulting array of `ProcInfo`.
+ *
+ * # Result
+ *
+ * This call succeeds (possibly with missing data, see below) unless we
+ * cannot allocate memory.
+ *
+ * # Performance
+ *
+ * - This call is always executed on a background thread.
+ * - This call does NOT wake up children processes.
+ * - This function is sometimes observably slow to resolve, in particular
+ * under Windows.
+ *
+ * # Error-handling and race conditions
+ *
+ * Requesting low-level information on a process and its threads is inherently
+ * subject to race conditions. Typically, if a process or a thread is killed
+ * while we're preparing to fetch information, we can easily end up with
+ * system/lib calls that return failures.
+ *
+ * For this reason, this API assumes that errors when placing a system/lib call
+ * are likely and normal. When some information cannot be obtained, the API will
+ * simply skip over said information.
+ *
+ * Note that due to different choices by OSes, the exact information we skip may
+ * vary across platforms. For instance, under Unix, failing to access the
+ * threads of a process will cause us to skip all data on the process, while
+ * under Windows, process information will be returned without thread
+ * information.
+ */
+RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests);
+
+/**
+ * Synchronous version of GetProcInfo.
+ */
+ProcInfoPromise::ResolveOrRejectValue GetProcInfoSync(
+ nsTArray<ProcInfoRequest>&& aRequests);
+
+/**
+ * Utility function: copy data from a `ProcInfo` and into either a
+ * `ParentProcInfoDictionary` or a `ChildProcInfoDictionary`.
+ */
+template <typename T>
+nsresult CopySysProcInfoToDOM(const ProcInfo& source, T* dest) {
+ // Copy system info.
+ dest->mPid = source.pid;
+ dest->mMemory = source.memory;
+ dest->mCpuTime = source.cpuTime;
+ dest->mCpuCycleCount = source.cpuCycleCount;
+
+ // Copy thread info.
+ mozilla::dom::Sequence<mozilla::dom::ThreadInfoDictionary> threads;
+ for (const ThreadInfo& entry : source.threads) {
+ mozilla::dom::ThreadInfoDictionary* thread =
+ threads.AppendElement(fallible);
+ if (NS_WARN_IF(!thread)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ thread->mCpuTime = entry.cpuTime;
+ thread->mCpuCycleCount = entry.cpuCycleCount;
+ thread->mTid = entry.tid;
+ thread->mName.Assign(entry.name);
+ }
+ dest->mThreads = std::move(threads);
+ return NS_OK;
+}
+
+} // namespace mozilla
+#endif // ProcInfo_h
diff --git a/toolkit/components/processtools/ProcInfo.mm b/toolkit/components/processtools/ProcInfo.mm
new file mode 100644
index 0000000000..6c98ce81f5
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo.mm
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ProcInfo.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+
+#include "nsMemoryReporterManager.h"
+
+#include <cstdio>
+#include <cstring>
+#include <unistd.h>
+
+#include <libproc.h>
+#include <sys/sysctl.h>
+#include <mach/mach.h>
+#include <mach/mach_time.h>
+
+static void GetTimeBase(mach_timebase_info_data_t* timebase) {
+ // Expected results are 125/3 on aarch64, and 1/1 on Intel CPUs.
+ if (mach_timebase_info(timebase) != KERN_SUCCESS) {
+ timebase->numer = 1;
+ timebase->denom = 1;
+ }
+}
+
+namespace mozilla {
+
+nsresult GetCpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ struct proc_taskinfo pti;
+ if ((unsigned long)proc_pidinfo(getpid(), PROC_PIDTASKINFO, 0, &pti,
+ PROC_PIDTASKINFO_SIZE) <
+ PROC_PIDTASKINFO_SIZE) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mach_timebase_info_data_t timebase;
+ GetTimeBase(&timebase);
+
+ *aResult = (pti.pti_total_user + pti.pti_total_system) * timebase.numer /
+ timebase.denom / PR_NSEC_PER_MSEC;
+ return NS_OK;
+}
+
+nsresult GetGpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ task_power_info_v2_data_t task_power_info;
+ mach_msg_type_number_t count = TASK_POWER_INFO_V2_COUNT;
+ kern_return_t kr = task_info(mach_task_self(), TASK_POWER_INFO_V2,
+ (task_info_t)&task_power_info, &count);
+ if (kr != KERN_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aResult = task_power_info.gpu_energy.task_gpu_utilisation / PR_NSEC_PER_MSEC;
+ return NS_OK;
+}
+
+int GetCycleTimeFrequencyMHz() { return 0; }
+
+ProcInfoPromise::ResolveOrRejectValue GetProcInfoSync(
+ nsTArray<ProcInfoRequest>&& aRequests) {
+ ProcInfoPromise::ResolveOrRejectValue result;
+
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(aRequests.Length())) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+
+ mach_timebase_info_data_t timebase;
+ GetTimeBase(&timebase);
+
+ for (const auto& request : aRequests) {
+ ProcInfo info;
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = std::move(request.origin);
+ info.windows = std::move(request.windowInfo);
+ info.utilityActors = std::move(request.utilityInfo);
+
+ struct proc_taskinfo pti;
+ if ((unsigned long)proc_pidinfo(request.pid, PROC_PIDTASKINFO, 0, &pti,
+ PROC_PIDTASKINFO_SIZE) <
+ PROC_PIDTASKINFO_SIZE) {
+ // Can't read data for this process.
+ // Probably either a sandboxing issue or a race condition, e.g.
+ // the process has been just been killed. Regardless, skip process.
+ continue;
+ }
+ info.cpuTime = (pti.pti_total_user + pti.pti_total_system) *
+ timebase.numer / timebase.denom;
+
+ mach_port_t selectedTask;
+ // If we did not get a task from a child process, we use mach_task_self()
+ if (request.childTask == MACH_PORT_NULL) {
+ selectedTask = mach_task_self();
+ } else {
+ selectedTask = request.childTask;
+ }
+
+ // The phys_footprint value (introduced in 10.11) of the TASK_VM_INFO data
+ // matches the value in the 'Memory' column of the Activity Monitor.
+ task_vm_info_data_t task_vm_info;
+ mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
+ kern_return_t kr = task_info(selectedTask, TASK_VM_INFO,
+ (task_info_t)&task_vm_info, &count);
+ info.memory = kr == KERN_SUCCESS ? task_vm_info.phys_footprint : 0;
+
+ // Now getting threads info
+
+ // task_threads() gives us a snapshot of the process threads
+ // but those threads can go away. All the code below makes
+ // the assumption that thread_info() calls may fail, and
+ // these errors will be ignored.
+ thread_act_port_array_t threadList;
+ mach_msg_type_number_t threadCount;
+ kern_return_t kret = task_threads(selectedTask, &threadList, &threadCount);
+ if (kret != KERN_SUCCESS) {
+ // For some reason, we have no data on the threads for this process.
+ // Most likely reason is that we have just lost a race condition and
+ // the process is dead.
+ // Let's stop here and ignore the entire process.
+ continue;
+ }
+
+ // Deallocate the thread list.
+ // Note that this deallocation is entirely undocumented, so the following
+ // code is based on guesswork and random examples found on the web.
+ auto guardThreadCount = MakeScopeExit([&] {
+ if (threadList == nullptr) {
+ return;
+ }
+ // Free each thread to avoid leaks.
+ for (mach_msg_type_number_t i = 0; i < threadCount; i++) {
+ mach_port_deallocate(mach_task_self(), threadList[i]);
+ }
+ vm_deallocate(mach_task_self(), /* address */ (vm_address_t)threadList,
+ /* size */ sizeof(thread_t) * threadCount);
+ });
+
+ for (mach_msg_type_number_t i = 0; i < threadCount; i++) {
+ // Basic thread info.
+ thread_extended_info_data_t threadInfoData;
+ count = THREAD_EXTENDED_INFO_COUNT;
+ kret = thread_info(threadList[i], THREAD_EXTENDED_INFO,
+ (thread_info_t)&threadInfoData, &count);
+ if (kret != KERN_SUCCESS) {
+ continue;
+ }
+
+ // Getting the thread id.
+ thread_identifier_info identifierInfo;
+ count = THREAD_IDENTIFIER_INFO_COUNT;
+ kret = thread_info(threadList[i], THREAD_IDENTIFIER_INFO,
+ (thread_info_t)&identifierInfo, &count);
+ if (kret != KERN_SUCCESS) {
+ continue;
+ }
+
+ // The two system calls were successful, let's add that thread
+ ThreadInfo* thread = info.threads.AppendElement(fallible);
+ if (!thread) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ thread->cpuTime =
+ threadInfoData.pth_user_time + threadInfoData.pth_system_time;
+ thread->name.AssignASCII(threadInfoData.pth_name);
+ thread->tid = identifierInfo.thread_id;
+ }
+
+ if (!gathered.put(request.pid, std::move(info))) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ }
+
+ result.SetResolve(std::move(gathered));
+ return result;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcInfo_bsd.cpp b/toolkit/components/processtools/ProcInfo_bsd.cpp
new file mode 100644
index 0000000000..a6ff488194
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_bsd.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ProcInfo.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "nsMemoryReporterManager.h"
+#include "nsWhitespaceTokenizer.h"
+
+#include <sys/types.h>
+#include <sys/sysctl.h>
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <unistd.h>
+
+namespace mozilla {
+
+int GetCycleTimeFrequencyMHz() { return 0; }
+
+nsresult GetCpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ timespec t;
+ if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t) == 0) {
+ uint64_t cpuTime =
+ uint64_t(t.tv_sec) * 1'000'000'000u + uint64_t(t.tv_nsec);
+ *aResult = cpuTime / PR_NSEC_PER_MSEC;
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult GetGpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+ProcInfoPromise::ResolveOrRejectValue GetProcInfoSync(
+ nsTArray<ProcInfoRequest>&& aRequests) {
+ ProcInfoPromise::ResolveOrRejectValue result;
+
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(aRequests.Length())) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ for (const auto& request : aRequests) {
+ size_t size;
+ int mib[6];
+ mib[0] = CTL_KERN;
+ mib[1] = KERN_PROC;
+ mib[2] = KERN_PROC_PID | KERN_PROC_SHOW_THREADS;
+ mib[3] = request.pid;
+ mib[4] = sizeof(kinfo_proc);
+ mib[5] = 0;
+ if (sysctl(mib, 6, nullptr, &size, nullptr, 0) == -1) {
+ // Can't get info for this process. Skip it.
+ continue;
+ }
+
+ mib[5] = size / sizeof(kinfo_proc);
+ auto procs = MakeUniqueFallible<kinfo_proc[]>(mib[5]);
+ if (!procs) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ if (sysctl(mib, 6, procs.get(), &size, nullptr, 0) == -1 &&
+ errno != ENOMEM) {
+ continue;
+ }
+
+ ProcInfo info;
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = request.origin;
+ info.windows = std::move(request.windowInfo);
+ info.utilityActors = std::move(request.utilityInfo);
+
+ bool found = false;
+ for (size_t i = 0; i < size / sizeof(kinfo_proc); i++) {
+ const auto& p = procs[i];
+ if (p.p_tid == -1) {
+ // This is the process.
+ found = true;
+ info.cpuTime = uint64_t(p.p_rtime_sec) * 1'000'000'000u +
+ uint64_t(p.p_rtime_usec) * 1'000u;
+ info.memory =
+ (p.p_vm_tsize + p.p_vm_dsize + p.p_vm_ssize) * getpagesize();
+ } else {
+ // This is one of its threads.
+ ThreadInfo threadInfo;
+ threadInfo.tid = p.p_tid;
+ threadInfo.cpuTime = uint64_t(p.p_rtime_sec) * 1'000'000'000u +
+ uint64_t(p.p_rtime_usec) * 1'000u;
+ info.threads.AppendElement(threadInfo);
+ }
+ }
+
+ if (found && !gathered.put(request.pid, std::move(info))) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ }
+
+ // ... and we're done!
+ result.SetResolve(std::move(gathered));
+ return result;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcInfo_common.cpp b/toolkit/components/processtools/ProcInfo_common.cpp
new file mode 100644
index 0000000000..1c5ade172e
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_common.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ProcInfo.h"
+
+#include "mozilla/UniquePtr.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
+ auto holder = MakeUnique<MozPromiseHolder<ProcInfoPromise>>();
+ RefPtr<ProcInfoPromise> promise = holder->Ensure(__func__);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to get stream transport service");
+ holder->Reject(rv, __func__);
+ return promise;
+ }
+
+ RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ __func__,
+ [holder = std::move(holder),
+ requests = std::move(aRequests)]() mutable -> void {
+ holder->ResolveOrReject(GetProcInfoSync(std::move(requests)), __func__);
+ });
+
+ rv = target->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to dispatch the LoadDataRunnable.");
+ }
+
+ return promise;
+}
+
+nsCString GetUtilityActorName(const UtilityActorName aActorName) {
+ switch (aActorName) {
+ case UtilityActorName::Unknown:
+ return "unknown"_ns;
+ case UtilityActorName::AudioDecoder_Generic:
+ return "audio-decoder-generic"_ns;
+ case UtilityActorName::AudioDecoder_AppleMedia:
+ return "audio-decoder-applemedia"_ns;
+ case UtilityActorName::AudioDecoder_WMF:
+ return "audio-decoder-wmf"_ns;
+ case UtilityActorName::MfMediaEngineCDM:
+ return "mf-media-engine"_ns;
+ case UtilityActorName::JSOracle:
+ return "js-oracle"_ns;
+ case UtilityActorName::WindowsUtils:
+ return "windows-utils"_ns;
+ case UtilityActorName::WindowsFileDialog:
+ return "windows-file-dialog"_ns;
+ default:
+ return "unknown"_ns;
+ }
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcInfo_linux.cpp b/toolkit/components/processtools/ProcInfo_linux.cpp
new file mode 100644
index 0000000000..51aa35c62e
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_linux.cpp
@@ -0,0 +1,344 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ProcInfo.h"
+#include "mozilla/ProcInfo_linux.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "nsMemoryReporterManager.h"
+#include "nsWhitespaceTokenizer.h"
+
+#include <cstdio>
+#include <cstring>
+#include <unistd.h>
+#include <dirent.h>
+
+#define NANOPERSEC 1000000000.
+
+namespace mozilla {
+
+int GetCycleTimeFrequencyMHz() { return 0; }
+
+// StatReader can parse and tokenize a POSIX stat file.
+// see http://man7.org/linux/man-pages/man5/proc.5.html
+//
+// Its usage is quite simple:
+//
+// StatReader reader(pid);
+// ProcInfo info;
+// rv = reader.ParseProc(info);
+// if (NS_FAILED(rv)) {
+// // the reading of the file or its parsing failed.
+// }
+//
+class StatReader {
+ public:
+ explicit StatReader(const base::ProcessId aPid)
+ : mPid(aPid), mMaxIndex(15), mTicksPerSec(sysconf(_SC_CLK_TCK)) {}
+
+ nsresult ParseProc(ProcInfo& aInfo) {
+ nsAutoString fileContent;
+ nsresult rv = ReadFile(fileContent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // We first extract the file or thread name
+ int32_t startPos = fileContent.RFindChar('(');
+ if (startPos == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ int32_t endPos = fileContent.RFindChar(')');
+ if (endPos == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ int32_t len = endPos - (startPos + 1);
+ mName.Assign(Substring(fileContent, startPos + 1, len));
+
+ // now we can use the tokenizer for the rest of the file
+ nsWhitespaceTokenizer tokenizer(Substring(fileContent, endPos + 2));
+ int32_t index = 2; // starting at third field
+ while (tokenizer.hasMoreTokens() && index < mMaxIndex) {
+ const nsAString& token = tokenizer.nextToken();
+ rv = UseToken(index, token, aInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+ index++;
+ }
+ return NS_OK;
+ }
+
+ protected:
+ // Called for each token found in the stat file.
+ nsresult UseToken(int32_t aIndex, const nsAString& aToken, ProcInfo& aInfo) {
+ // We're using a subset of what stat has to offer for now.
+ nsresult rv = NS_OK;
+ // see the proc documentation for fields index references.
+ switch (aIndex) {
+ case 13:
+ // Amount of time that this process has been scheduled
+ // in user mode, measured in clock ticks
+ aInfo.cpuTime += GetCPUTime(aToken, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case 14:
+ // Amount of time that this process has been scheduled
+ // in kernel mode, measured in clock ticks
+ aInfo.cpuTime += GetCPUTime(aToken, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ return rv;
+ }
+
+ // Converts a token into a int64_t
+ uint64_t Get64Value(const nsAString& aToken, nsresult* aRv) {
+ // We can't use aToken.ToInteger64() since it returns a signed 64.
+ // and that can result into an overflow.
+ nsresult rv = NS_OK;
+ uint64_t out = 0;
+ if (sscanf(NS_ConvertUTF16toUTF8(aToken).get(), "%" PRIu64, &out) == 0) {
+ rv = NS_ERROR_FAILURE;
+ }
+ *aRv = rv;
+ return out;
+ }
+
+ // Converts a token into CPU time in nanoseconds.
+ uint64_t GetCPUTime(const nsAString& aToken, nsresult* aRv) {
+ nsresult rv;
+ uint64_t value = Get64Value(aToken, &rv);
+ *aRv = rv;
+ if (NS_FAILED(rv)) {
+ return 0;
+ }
+ if (value) {
+ value = (value * NANOPERSEC) / mTicksPerSec;
+ }
+ return value;
+ }
+
+ base::ProcessId mPid;
+ int32_t mMaxIndex;
+ nsCString mFilepath;
+ nsString mName;
+
+ private:
+ // Reads the stat file and puts its content in a nsString.
+ nsresult ReadFile(nsAutoString& aFileContent) {
+ if (mFilepath.IsEmpty()) {
+ if (mPid == 0) {
+ mFilepath.AssignLiteral("/proc/self/stat");
+ } else {
+ mFilepath.AppendPrintf("/proc/%u/stat", unsigned(mPid));
+ }
+ }
+ FILE* fstat = fopen(mFilepath.get(), "r");
+ if (!fstat) {
+ return NS_ERROR_FAILURE;
+ }
+ // /proc is a virtual file system and all files are
+ // of size 0, so GetFileSize() and related functions will
+ // return 0 - so the way to read the file is to fill a buffer
+ // of an arbitrary big size and look for the end of line char.
+ char buffer[2048];
+ char* end;
+ char* start = fgets(buffer, 2048, fstat);
+ fclose(fstat);
+ if (start == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+ // let's find the end
+ end = strchr(buffer, '\n');
+ if (!end) {
+ return NS_ERROR_FAILURE;
+ }
+ aFileContent.AssignASCII(buffer, size_t(end - start));
+ return NS_OK;
+ }
+
+ int64_t mTicksPerSec;
+};
+
+// Threads have the same stat file. The only difference is its path
+// and we're getting less info in the ThreadInfo structure.
+class ThreadInfoReader final : public StatReader {
+ public:
+ ThreadInfoReader(const base::ProcessId aPid, const base::ProcessId aTid)
+ : StatReader(aPid) {
+ mFilepath.AppendPrintf("/proc/%u/task/%u/stat", unsigned(aPid),
+ unsigned(aTid));
+ }
+
+ nsresult ParseThread(ThreadInfo& aInfo) {
+ ProcInfo info;
+ nsresult rv = StatReader::ParseProc(info);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Copying over the data we got from StatReader::ParseProc()
+ aInfo.cpuTime = info.cpuTime;
+ aInfo.name.Assign(mName);
+ return NS_OK;
+ }
+};
+
+nsresult GetCpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ timespec t;
+ if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t) == 0) {
+ uint64_t cpuTime =
+ uint64_t(t.tv_sec) * 1'000'000'000u + uint64_t(t.tv_nsec);
+ *aResult = cpuTime / PR_NSEC_PER_MSEC;
+ return NS_OK;
+ }
+
+ StatReader reader(0);
+ ProcInfo info;
+ nsresult rv = reader.ParseProc(info);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ *aResult = info.cpuTime / PR_NSEC_PER_MSEC;
+ return NS_OK;
+}
+
+nsresult GetGpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+ProcInfoPromise::ResolveOrRejectValue GetProcInfoSync(
+ nsTArray<ProcInfoRequest>&& aRequests) {
+ ProcInfoPromise::ResolveOrRejectValue result;
+
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(aRequests.Length())) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ for (const auto& request : aRequests) {
+ ProcInfo info;
+
+ timespec t;
+ clockid_t clockid = MAKE_PROCESS_CPUCLOCK(request.pid, CPUCLOCK_SCHED);
+ if (clock_gettime(clockid, &t) == 0) {
+ info.cpuTime = uint64_t(t.tv_sec) * 1'000'000'000u + uint64_t(t.tv_nsec);
+ } else {
+ // Fallback to parsing /proc/<pid>/stat
+ StatReader reader(request.pid);
+ nsresult rv = reader.ParseProc(info);
+ if (NS_FAILED(rv)) {
+ // Can't read data for this proc.
+ // Probably either a sandboxing issue or a race condition, e.g.
+ // the process has been just been killed. Regardless, skip process.
+ continue;
+ }
+ }
+
+ // The 'Memory' value displayed in the system monitor is resident -
+ // shared. statm contains more fields, but we're only interested in
+ // the first three.
+ static const int MAX_FIELD = 3;
+ size_t VmSize, resident, shared;
+ info.memory = 0;
+ FILE* f = fopen(nsPrintfCString("/proc/%u/statm", request.pid).get(), "r");
+ if (f) {
+ int nread = fscanf(f, "%zu %zu %zu", &VmSize, &resident, &shared);
+ fclose(f);
+ if (nread == MAX_FIELD) {
+ info.memory = (resident - shared) * getpagesize();
+ }
+ }
+
+ // Extra info
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = request.origin;
+ info.windows = std::move(request.windowInfo);
+ info.utilityActors = std::move(request.utilityInfo);
+
+ // Let's look at the threads
+ nsCString taskPath;
+ taskPath.AppendPrintf("/proc/%u/task", unsigned(request.pid));
+ DIR* dirHandle = opendir(taskPath.get());
+ if (!dirHandle) {
+ // For some reason, we have no data on the threads for this process.
+ // Most likely reason is that we have just lost a race condition and
+ // the process is dead.
+ // Let's stop here and ignore the entire process.
+ continue;
+ }
+ auto cleanup = mozilla::MakeScopeExit([&] { closedir(dirHandle); });
+
+ // If we can't read some thread info, we ignore that thread.
+ dirent* entry;
+ while ((entry = readdir(dirHandle)) != nullptr) {
+ if (entry->d_name[0] == '.') {
+ continue;
+ }
+ nsAutoCString entryName(entry->d_name);
+ nsresult rv;
+ int32_t tid = entryName.ToInteger(&rv);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ ThreadInfo threadInfo;
+ threadInfo.tid = tid;
+
+ timespec ts;
+ if (clock_gettime(MAKE_THREAD_CPUCLOCK(tid, CPUCLOCK_SCHED), &ts) == 0) {
+ threadInfo.cpuTime =
+ uint64_t(ts.tv_sec) * 1'000'000'000u + uint64_t(ts.tv_nsec);
+
+ nsCString path;
+ path.AppendPrintf("/proc/%u/task/%u/comm", unsigned(request.pid),
+ unsigned(tid));
+ FILE* fstat = fopen(path.get(), "r");
+ if (fstat) {
+ // /proc is a virtual file system and all files are
+ // of size 0, so GetFileSize() and related functions will
+ // return 0 - so the way to read the file is to fill a buffer
+ // of an arbitrary big size and look for the end of line char.
+ // The size of the buffer needs to be as least 16, which is the
+ // value of TASK_COMM_LEN in the Linux kernel.
+ char buffer[32];
+ char* start = fgets(buffer, sizeof(buffer), fstat);
+ fclose(fstat);
+ if (start) {
+ // The thread name should always be smaller than our buffer,
+ // so we should find a newline character.
+ char* end = strchr(buffer, '\n');
+ if (end) {
+ threadInfo.name.AssignASCII(buffer, size_t(end - start));
+ info.threads.AppendElement(threadInfo);
+ continue;
+ }
+ }
+ }
+ }
+
+ // Fallback to parsing /proc/<pid>/task/<tid>/stat
+ // This is needed for child processes, as access to the per-thread
+ // CPU clock is restricted to the process owning the thread.
+ ThreadInfoReader reader(request.pid, tid);
+ rv = reader.ParseThread(threadInfo);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ info.threads.AppendElement(threadInfo);
+ }
+
+ if (!gathered.put(request.pid, std::move(info))) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ }
+
+ // ... and we're done!
+ result.SetResolve(std::move(gathered));
+ return result;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcInfo_linux.h b/toolkit/components/processtools/ProcInfo_linux.h
new file mode 100644
index 0000000000..7be1ca5cb7
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_linux.h
@@ -0,0 +1,26 @@
+/* -*- 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 __mozilla_ProcInfo_linux_h
+#define __mozilla_ProcInfo_linux_h
+
+// The following is directly inspired from kernel:
+// https://github.com/torvalds/linux/blob/v5.16/include/linux/posix-timers.h#L29-L48
+#ifndef CPUCLOCK_SCHED
+# define CPUCLOCK_SCHED 2
+#endif
+#ifndef CPUCLOCK_PERTHREAD_MASK
+# define CPUCLOCK_PERTHREAD_MASK 4
+#endif
+#ifndef MAKE_PROCESS_CPUCLOCK
+# define MAKE_PROCESS_CPUCLOCK(pid, clock) \
+ ((int)(~(unsigned)(pid) << 3) | (int)(clock))
+#endif
+#ifndef MAKE_THREAD_CPUCLOCK
+# define MAKE_THREAD_CPUCLOCK(tid, clock) \
+ MAKE_PROCESS_CPUCLOCK(tid, (clock) | CPUCLOCK_PERTHREAD_MASK)
+#endif
+
+#endif // ProcInfo_linux_h
diff --git a/toolkit/components/processtools/ProcInfo_solaris.cpp b/toolkit/components/processtools/ProcInfo_solaris.cpp
new file mode 100644
index 0000000000..c20f731b60
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_solaris.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ProcInfo.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "nsMemoryReporterManager.h"
+#include "nsWhitespaceTokenizer.h"
+
+#include <cerrno>
+#include <cstdio>
+#include <cstring>
+#include <fcntl.h>
+#include <procfs.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <stdio.h>
+#include <unistd.h>
+
+// Maximal length on /proc filename
+// "/proc/999999/lwp/999999/lwpsinfo"
+// "/proc/999999/psinfo"
+#define FNMAX_PROCFS "/proc/999999/lwp/999999/lwpsinfo"
+
+namespace mozilla {
+
+int GetCycleTimeFrequencyMHz() { return 0; }
+
+nsresult GetCpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ timespec t;
+ if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &t) == 0) {
+ uint64_t cpuTime =
+ uint64_t(t.tv_sec) * 1'000'000'000u + uint64_t(t.tv_nsec);
+ *aResult = cpuTime / PR_NSEC_PER_MSEC;
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+nsresult GetGpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+ProcInfoPromise::ResolveOrRejectValue GetProcInfoSync(
+ nsTArray<ProcInfoRequest>&& aRequests) {
+ ProcInfoPromise::ResolveOrRejectValue result;
+
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(aRequests.Length())) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ for (const auto& request : aRequests) {
+ ProcInfo info;
+
+ int fd;
+ char filename[sizeof(FNMAX_PROCFS)];
+ psinfo_t psinfo;
+ snprintf(filename, sizeof(FNMAX_PROCFS), "/proc/%d/psinfo", request.pid);
+ if ((fd = open(filename, O_RDONLY)) == -1) {
+ result.SetReject(NS_ERROR_FAILURE);
+ return result;
+ }
+ if (read(fd, &psinfo, sizeof(psinfo_t)) != sizeof(psinfo_t)) {
+ close(fd);
+ result.SetReject(NS_ERROR_FAILURE);
+ return result;
+ }
+ close(fd);
+ info.cpuTime =
+ psinfo.pr_time.tv_sec * 1'000'000'000u + psinfo.pr_time.tv_nsec;
+ info.memory = psinfo.pr_rssize * 1024;
+
+ // Extra info
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = request.origin;
+ info.windows = std::move(request.windowInfo);
+ info.utilityActors = std::move(request.utilityInfo);
+
+ // Let's look at the threads
+ for (int lwp = 1, lwp_max = psinfo.pr_nlwp; lwp <= lwp_max; lwp++) {
+ lwpsinfo_t lwpsinfo;
+ snprintf(filename, sizeof(FNMAX_PROCFS), "/proc/%d/lwp/%d/lwpsinfo",
+ request.pid, lwp);
+ if ((fd = open(filename, O_RDONLY)) == -1) {
+ // Some LWPs might no longer exist. That just means that there are
+ // probably others with bigger LWP id. But we need to limit the search
+ // because of possible race condition.
+ if (lwp_max < 2 * psinfo.pr_nlwp) lwp_max++;
+ continue;
+ }
+ if (read(fd, &lwpsinfo, sizeof(lwpsinfo_t)) != sizeof(lwpsinfo_t)) {
+ close(fd);
+ continue;
+ }
+ close(fd);
+ ThreadInfo threadInfo;
+ threadInfo.tid = lwpsinfo.pr_lwpid;
+ threadInfo.cpuTime =
+ lwpsinfo.pr_time.tv_sec * 1'000'000'000u + lwpsinfo.pr_time.tv_nsec;
+ threadInfo.name.AssignASCII(lwpsinfo.pr_name, strlen(lwpsinfo.pr_name));
+ info.threads.AppendElement(threadInfo);
+ }
+
+ if (!gathered.put(request.pid, std::move(info))) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ }
+
+ // ... and we're done!
+ result.SetResolve(std::move(gathered));
+ return result;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcInfo_win.cpp b/toolkit/components/processtools/ProcInfo_win.cpp
new file mode 100644
index 0000000000..02b63190ed
--- /dev/null
+++ b/toolkit/components/processtools/ProcInfo_win.cpp
@@ -0,0 +1,295 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ProcInfo.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "mozilla/SSE.h"
+#include "gfxWindowsPlatform.h"
+#include "nsMemoryReporterManager.h"
+#include "nsWindowsHelpers.h"
+#include <windows.h>
+#include <psapi.h>
+#include <winternl.h>
+#include <xpcpublic.h>
+
+#ifndef STATUS_INFO_LENGTH_MISMATCH
+# define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L)
+#endif
+
+#define PR_USEC_PER_NSEC 1000L
+
+typedef HRESULT(WINAPI* GETTHREADDESCRIPTION)(HANDLE hThread,
+ PWSTR* threadDescription);
+
+namespace mozilla {
+
+static uint64_t ToNanoSeconds(const FILETIME& aFileTime) {
+ // FILETIME values are 100-nanoseconds units, converting
+ ULARGE_INTEGER usec = {{aFileTime.dwLowDateTime, aFileTime.dwHighDateTime}};
+ return usec.QuadPart * 100;
+}
+
+int GetCpuFrequencyMHz() {
+ static const int frequency = []() {
+ // Get the nominal CPU frequency.
+ HKEY key;
+ static const WCHAR keyName[] =
+ L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0";
+
+ if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, keyName, 0, KEY_QUERY_VALUE, &key) ==
+ ERROR_SUCCESS) {
+ DWORD data, len;
+ len = sizeof(data);
+
+ if (RegQueryValueEx(key, L"~Mhz", 0, 0, reinterpret_cast<LPBYTE>(&data),
+ &len) == ERROR_SUCCESS) {
+ return static_cast<int>(data);
+ }
+ }
+
+ return 0;
+ }();
+
+ return frequency;
+}
+
+int GetCycleTimeFrequencyMHz() {
+ static const int frequency = []() {
+ // Having a constant TSC is required to convert cycle time to actual time.
+ // In automation, having short CPU times reported as 0 is more of a problem
+ // than having an imprecise value. The fallback method can't report CPU
+ // times < 1/64s.
+ if (!mozilla::has_constant_tsc() && !xpc::IsInAutomation()) {
+ return 0;
+ }
+
+ return GetCpuFrequencyMHz();
+ }();
+
+ return frequency;
+}
+
+nsresult GetCpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ int frequencyInMHz = GetCycleTimeFrequencyMHz();
+ if (frequencyInMHz) {
+ uint64_t cpuCycleCount;
+ if (!QueryProcessCycleTime(::GetCurrentProcess(), &cpuCycleCount)) {
+ return NS_ERROR_FAILURE;
+ }
+ constexpr int HZ_PER_MHZ = 1000000;
+ *aResult =
+ cpuCycleCount / (frequencyInMHz * (HZ_PER_MHZ / PR_MSEC_PER_SEC));
+ return NS_OK;
+ }
+
+ FILETIME createTime, exitTime, kernelTime, userTime;
+ if (!GetProcessTimes(::GetCurrentProcess(), &createTime, &exitTime,
+ &kernelTime, &userTime)) {
+ return NS_ERROR_FAILURE;
+ }
+ *aResult =
+ (ToNanoSeconds(kernelTime) + ToNanoSeconds(userTime)) / PR_NSEC_PER_MSEC;
+ return NS_OK;
+}
+
+nsresult GetGpuTimeSinceProcessStartInMs(uint64_t* aResult) {
+ return gfxWindowsPlatform::GetGpuTimeSinceProcessStartInMs(aResult);
+}
+
+ProcInfoPromise::ResolveOrRejectValue GetProcInfoSync(
+ nsTArray<ProcInfoRequest>&& aRequests) {
+ ProcInfoPromise::ResolveOrRejectValue result;
+
+ HashMap<base::ProcessId, ProcInfo> gathered;
+ if (!gathered.reserve(aRequests.Length())) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+
+ int frequencyInMHz = GetCycleTimeFrequencyMHz();
+
+ // ---- Copying data on processes (minus threads).
+
+ for (const auto& request : aRequests) {
+ nsAutoHandle handle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
+ FALSE, request.pid));
+
+ if (!handle) {
+ // Ignore process, it may have died.
+ continue;
+ }
+
+ uint64_t cpuCycleTime;
+ if (!QueryProcessCycleTime(handle.get(), &cpuCycleTime)) {
+ // Ignore process, it may have died.
+ continue;
+ }
+
+ uint64_t cpuTime;
+ if (frequencyInMHz) {
+ cpuTime = cpuCycleTime * PR_USEC_PER_NSEC / frequencyInMHz;
+ } else {
+ FILETIME createTime, exitTime, kernelTime, userTime;
+ if (!GetProcessTimes(handle.get(), &createTime, &exitTime, &kernelTime,
+ &userTime)) {
+ // Ignore process, it may have died.
+ continue;
+ }
+ cpuTime = ToNanoSeconds(kernelTime) + ToNanoSeconds(userTime);
+ }
+
+ PROCESS_MEMORY_COUNTERS_EX memoryCounters;
+ if (!GetProcessMemoryInfo(handle.get(),
+ (PPROCESS_MEMORY_COUNTERS)&memoryCounters,
+ sizeof(memoryCounters))) {
+ // Ignore process, it may have died.
+ continue;
+ }
+
+ // Assumption: values of `pid` are distinct between processes,
+ // regardless of any race condition we might have stumbled upon. Even
+ // if it somehow could happen, in the worst case scenario, we might
+ // end up overwriting one process info and we might end up with too
+ // many threads attached to a process, as the data is not crucial, we
+ // do not need to defend against that (unlikely) scenario.
+ ProcInfo info;
+ info.pid = request.pid;
+ info.childId = request.childId;
+ info.type = request.processType;
+ info.origin = request.origin;
+ info.windows = std::move(request.windowInfo);
+ info.utilityActors = std::move(request.utilityInfo);
+ info.cpuTime = cpuTime;
+ info.cpuCycleCount = cpuCycleTime;
+ info.memory = memoryCounters.PrivateUsage;
+
+ if (!gathered.put(request.pid, std::move(info))) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+ }
+
+ // ---- Add thread data to already-copied processes.
+
+ NTSTATUS ntStatus;
+
+ UniquePtr<char[]> buf;
+ ULONG bufLen = 512u * 1024u;
+
+ // We must query for information in a loop, since we are effectively asking
+ // the kernel to take a snapshot of all the processes on the system;
+ // the size of the required buffer may fluctuate between successive calls.
+ do {
+ // These allocations can be hundreds of megabytes on some computers, so
+ // we should use fallible new here.
+ buf = MakeUniqueFallible<char[]>(bufLen);
+ if (!buf) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+
+ ntStatus = ::NtQuerySystemInformation(SystemProcessInformation, buf.get(),
+ bufLen, &bufLen);
+ if (ntStatus != STATUS_INFO_LENGTH_MISMATCH) {
+ break;
+ }
+
+ // If we need another NtQuerySystemInformation call, allocate a
+ // slightly larger buffer than what would have been needed this time,
+ // to account for possible process or thread creations that might
+ // happen between our calls.
+ bufLen += 8u * 1024u;
+ } while (true);
+ if (!NT_SUCCESS(ntStatus)) {
+ result.SetReject(NS_ERROR_UNEXPECTED);
+ return result;
+ }
+
+ // `GetThreadDescription` is available as of Windows 10.
+ // We attempt to import it dynamically, knowing that it
+ // may be `nullptr`.
+ auto getThreadDescription =
+ reinterpret_cast<GETTHREADDESCRIPTION>(::GetProcAddress(
+ ::GetModuleHandleW(L"Kernel32.dll"), "GetThreadDescription"));
+
+ PSYSTEM_PROCESS_INFORMATION processInfo;
+ for (ULONG offset = 0;; offset += processInfo->NextEntryOffset) {
+ MOZ_RELEASE_ASSERT(offset < bufLen);
+ processInfo =
+ reinterpret_cast<PSYSTEM_PROCESS_INFORMATION>(buf.get() + offset);
+ ULONG pid = HandleToUlong(processInfo->UniqueProcessId);
+ // Check if we are interested in this process.
+ auto processLookup = gathered.lookup(pid);
+ if (processLookup) {
+ for (ULONG i = 0; i < processInfo->NumberOfThreads; ++i) {
+ // The thread information structs are stored in the buffer right
+ // after the SYSTEM_PROCESS_INFORMATION struct.
+ PSYSTEM_THREAD_INFORMATION thread =
+ reinterpret_cast<PSYSTEM_THREAD_INFORMATION>(
+ buf.get() + offset + sizeof(SYSTEM_PROCESS_INFORMATION) +
+ sizeof(SYSTEM_THREAD_INFORMATION) * i);
+ ULONG tid = HandleToUlong(thread->ClientId.UniqueThread);
+
+ ThreadInfo* threadInfo =
+ processLookup->value().threads.AppendElement(fallible);
+ if (!threadInfo) {
+ result.SetReject(NS_ERROR_OUT_OF_MEMORY);
+ return result;
+ }
+
+ nsAutoHandle hThread(
+ OpenThread(/* dwDesiredAccess = */ THREAD_QUERY_INFORMATION,
+ /* bInheritHandle = */ FALSE,
+ /* dwThreadId = */ tid));
+ if (!hThread) {
+ // Cannot open thread. Not sure why, but let's erase this thread
+ // and attempt to find data on other threads.
+ processLookup->value().threads.RemoveLastElement();
+ continue;
+ }
+
+ threadInfo->tid = tid;
+
+ // Attempt to get thread times.
+ // If we fail, continue without this piece of information.
+ if (QueryThreadCycleTime(hThread.get(), &threadInfo->cpuCycleCount) &&
+ frequencyInMHz) {
+ threadInfo->cpuTime =
+ threadInfo->cpuCycleCount * PR_USEC_PER_NSEC / frequencyInMHz;
+ } else {
+ FILETIME createTime, exitTime, kernelTime, userTime;
+ if (GetThreadTimes(hThread.get(), &createTime, &exitTime, &kernelTime,
+ &userTime)) {
+ threadInfo->cpuTime =
+ ToNanoSeconds(kernelTime) + ToNanoSeconds(userTime);
+ }
+ }
+
+ // Attempt to get thread name.
+ // If we fail, continue without this piece of information.
+ if (getThreadDescription) {
+ PWSTR threadName = nullptr;
+ if (getThreadDescription(hThread.get(), &threadName) && threadName) {
+ threadInfo->name = threadName;
+ }
+ if (threadName) {
+ LocalFree(threadName);
+ }
+ }
+ }
+ }
+
+ if (processInfo->NextEntryOffset == 0) {
+ break;
+ }
+ }
+
+ // ----- We're ready to return.
+ result.SetResolve(std::move(gathered));
+ return result;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/processtools/ProcessToolsService.cpp b/toolkit/components/processtools/ProcessToolsService.cpp
new file mode 100644
index 0000000000..3855996fdf
--- /dev/null
+++ b/toolkit/components/processtools/ProcessToolsService.cpp
@@ -0,0 +1,33 @@
+/* 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 "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPtr.h"
+#include "nsCOMPtr.h"
+#include "ProcessToolsService.h"
+
+// This anonymous namespace prevents outside C++ code from improperly accessing
+// these implementation details.
+namespace {
+extern "C" {
+// Implemented in Rust.
+void new_process_tools_service(nsIProcessToolsService** result);
+}
+
+static mozilla::StaticRefPtr<nsIProcessToolsService> sProcessToolsService;
+} // namespace
+
+already_AddRefed<nsIProcessToolsService> GetProcessToolsService() {
+ nsCOMPtr<nsIProcessToolsService> processToolsService;
+
+ if (sProcessToolsService) {
+ processToolsService = sProcessToolsService;
+ } else {
+ new_process_tools_service(getter_AddRefs(processToolsService));
+ sProcessToolsService = processToolsService;
+ mozilla::ClearOnShutdown(&sProcessToolsService);
+ }
+
+ return processToolsService.forget();
+}
diff --git a/toolkit/components/processtools/ProcessToolsService.h b/toolkit/components/processtools/ProcessToolsService.h
new file mode 100644
index 0000000000..932fd5d4eb
--- /dev/null
+++ b/toolkit/components/processtools/ProcessToolsService.h
@@ -0,0 +1,8 @@
+/* -*- Mode: C++; tab-width: 2; 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 "nsIProcessToolsService.h"
+
+already_AddRefed<nsIProcessToolsService> GetProcessToolsService();
diff --git a/toolkit/components/processtools/components.conf b/toolkit/components/processtools/components.conf
new file mode 100644
index 0000000000..6e7aede0dc
--- /dev/null
+++ b/toolkit/components/processtools/components.conf
@@ -0,0 +1,10 @@
+Classes = [
+ {
+ 'cid': '{79A13656-A472-4713-B0E1-AB39A15CF790}',
+ 'contract_ids': ["@mozilla.org/processtools-service;1"],
+ 'type': 'nsIProcessToolsService',
+ 'constructor': 'GetProcessToolsService',
+ 'singleton': True,
+ 'headers': ['ProcessToolsService.h'],
+ }
+]
diff --git a/toolkit/components/processtools/metrics.yaml b/toolkit/components/processtools/metrics.yaml
new file mode 100644
index 0000000000..6efbd22fa5
--- /dev/null
+++ b/toolkit/components/processtools/metrics.yaml
@@ -0,0 +1,337 @@
+# 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/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - "Core :: DOM: Content Processes"
+
+power:
+ cpu_time_bogus_values:
+ type: counter
+ description: >
+ Impossibly large CPU time values that were discarded.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1755733
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1755733
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ telemetry_mirror: POWER_CPU_TIME_BOGUS_VALUES
+
+ cpu_time_per_process_type_ms:
+ type: labeled_counter
+ description: >
+ CPU time used by each process type in ms.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1747138
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1747138
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ labels: &per_process_type_labels
+ - parent.active
+ - parent.active.playing-audio
+ - parent.active.playing-video
+ - parent.inactive
+ - parent.inactive.playing-audio
+ - parent.inactive.playing-video
+ - prealloc
+ - privilegedabout
+ - rdd
+ - socket
+ - web.background
+ - web.background-perceivable
+ - web.foreground
+ - extension
+ - gpu
+ - gmplugin
+ - utility
+ telemetry_mirror: POWER_CPU_TIME_PER_PROCESS_TYPE_MS
+
+ cpu_time_per_tracker_type_ms:
+ type: labeled_counter
+ description: >
+ CPU time used by content processes used only for tracking resources,
+ labeled by the category of the tracker.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1802361
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1802361
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ labels:
+ - ad
+ - analytics
+ - cryptomining
+ - fingerprinting
+ - social
+ - unknown
+
+ gpu_time_per_process_type_ms:
+ type: labeled_counter
+ description: >
+ GPU time used by each process type in ms.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1747138
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1747138
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ labels: *per_process_type_labels
+ telemetry_mirror: POWER_GPU_TIME_PER_PROCESS_TYPE_MS
+
+ gpu_time_bogus_values:
+ type: counter
+ description: >
+ Impossibly large GPU time values that were discarded.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1755733
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1755733
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ telemetry_mirror: POWER_GPU_TIME_BOGUS_VALUES
+
+ wakeups_per_process_type:
+ type: labeled_counter
+ description: >
+ How many times threads woke up and could have woken up a CPU core.
+ Broken down by process type.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1759535
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1759535
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ labels: *per_process_type_labels
+ telemetry_mirror: POWER_WAKEUPS_PER_PROCESS_TYPE
+
+ total_cpu_time_ms:
+ type: counter
+ description: >
+ Total CPU time used by all processes in ms.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1736040
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1736040
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ telemetry_mirror: POWER_TOTAL_CPU_TIME_MS
+
+ total_gpu_time_ms:
+ type: counter
+ description: >
+ Total GPU time used by all processes in ms.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1743176
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1743176
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ telemetry_mirror: POWER_TOTAL_GPU_TIME_MS
+
+ total_thread_wakeups:
+ type: counter
+ description: >
+ How many times threads woke up and could have woken up a CPU core.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1759535
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1759535
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ telemetry_mirror: POWER_TOTAL_THREAD_WAKEUPS
+
+power.wakeups_per_thread:
+ parent_active: &per_thread_wakeups
+ type: labeled_counter
+ description: >
+ How many times threads woke up and could have woken up a CPU core.
+ Broken down by thread name for a given process type.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1763474
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1763474
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ labels: &per_thread_labels
+ - asynclogger
+ - audioipc
+ - audioipc_callback_rpc
+ - audioipc_client_callback
+ - audioipc_client_rpc
+ - audioipc_devicecollection_rpc
+ - audioipc_server_callback
+ - audioipc_server_rpc
+ - backgroundthreadpool
+ - bgiothreadpool
+ - bgreadurls
+ - bhmgr_monitor
+ - bhmgr_processor
+ - cameras_ipc
+ - capturethread
+ - classifier_update
+ - com_mta
+ - compositor
+ - cookie
+ - cubeboperation
+ - datachannel_io
+ - dns_resolver
+ - dom_worker
+ - domcachethread
+ - extensionprotocolhandler
+ - font_loader
+ - fontenumthread
+ - fs_broker
+ - geckomain
+ - gmpthread
+ - graphrunner
+ - html5_parser
+ - imagebridgechld
+ - imageio
+ - indexeddb
+ - initfontlist
+ - inotifyeventthread
+ - ipc_i_o_child
+ - ipc_i_o_parent
+ - ipc_launch
+ - ipdl_background
+ - js_watchdog
+ - jump_list
+ - libwebrtcmodulethread
+ - link_monitor
+ - ls_thread
+ - mediacache
+ - mediadecoderstatemachine
+ - mediapdecoder
+ - mediasupervisor
+ - mediatimer
+ - mediatrackgrph
+ - memorypoller
+ - mozstorage
+ - mtransport
+ - netlink_monitor
+ - pacerthread
+ - permission
+ - playeventsound
+ - processhangmon
+ - profilerchild
+ - proxyresolution
+ - quotamanager_io
+ - registerfonts
+ - remotebackbuffer
+ - remotelzystream
+ - remvidchild
+ - renderer
+ - sandboxreporter
+ - savescripts
+ - socket_thread
+ - softwarevsyncthread
+ - ssl_cert
+ - startupcache
+ - streamtrans
+ - stylethread
+ - swcomposite
+ - taskcontroller
+ - timer
+ - toastbgthread
+ - trr_background
+ - untrusted_modules
+ - url_classifier
+ - videocapture
+ - vsynciothread
+ - webrtccallthread
+ - webrtcworker
+ - wincompositor
+ - windowsvsyncthread
+ - winwindowocclusioncalc
+ - worker_launcher
+ - wrrenderbackend
+ - wrscenebuilder
+ - wrscenebuilderlp
+ - wrworker
+ - wrworkerlp
+
+ parent_inactive: *per_thread_wakeups
+ content_foreground: *per_thread_wakeups
+ content_background: *per_thread_wakeups
+ gpu_process: *per_thread_wakeups
+
+power.cpu_ms_per_thread:
+ parent_active: &per_thread_cpu_ms
+ type: labeled_counter
+ description: >
+ How many miliseconds of CPU time were used.
+ Broken down by thread name for a given process type.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1763474
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1763474
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
+ labels: *per_thread_labels
+
+ parent_inactive: *per_thread_cpu_ms
+ content_foreground: *per_thread_cpu_ms
+ content_background: *per_thread_cpu_ms
+ gpu_process: *per_thread_cpu_ms
+
+power.battery:
+ percentage_when_user_active:
+ type: custom_distribution
+ description: >
+ Records how many percent of battery was available for each period of
+ user activity.
+ range_min: 0
+ range_max: 100
+ bucket_count: 100
+ histogram_type: linear
+ unit: percent
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1769255
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1769255
+ data_sensitivity:
+ - interaction
+ - technical
+ notification_emails:
+ - florian@mozilla.com
+ expires: never
diff --git a/toolkit/components/processtools/moz.build b/toolkit/components/processtools/moz.build
new file mode 100644
index 0000000000..d45fe87237
--- /dev/null
+++ b/toolkit/components/processtools/moz.build
@@ -0,0 +1,53 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Performance Monitoring")
+
+XPIDL_MODULE = "toolkit_processtools"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+XPIDL_SOURCES += [
+ "nsIProcessToolsService.idl",
+]
+
+EXPORTS.mozilla += [
+ "ProcInfo.h",
+ "ProcInfo_linux.h",
+]
+
+EXPORTS += [
+ "ProcessToolsService.h",
+]
+
+UNIFIED_SOURCES += [
+ "ProcessToolsService.cpp",
+ "ProcInfo_common.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+XPCSHELL_TESTS_MANIFESTS += ["tests/xpcshell/xpcshell.toml"]
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+# Platform-specific implementations of `ProcInfo`.
+toolkit = CONFIG["MOZ_WIDGET_TOOLKIT"]
+if toolkit == "gtk" or toolkit == "android":
+ if CONFIG["OS_TARGET"] == "OpenBSD":
+ UNIFIED_SOURCES += ["ProcInfo_bsd.cpp"]
+ elif CONFIG["OS_TARGET"] == "SunOS":
+ UNIFIED_SOURCES += ["ProcInfo_solaris.cpp"]
+ else:
+ UNIFIED_SOURCES += ["ProcInfo_linux.cpp"]
+elif toolkit == "windows":
+ UNIFIED_SOURCES += ["ProcInfo_win.cpp"]
+elif toolkit == "cocoa":
+ UNIFIED_SOURCES += ["ProcInfo.mm"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/toolkit/components/processtools/nsIProcessToolsService.idl b/toolkit/components/processtools/nsIProcessToolsService.idl
new file mode 100644
index 0000000000..4298cdddf8
--- /dev/null
+++ b/toolkit/components/processtools/nsIProcessToolsService.idl
@@ -0,0 +1,48 @@
+/* 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 "nsISupports.idl"
+
+[scriptable, uuid(1341f571-ebed-4305-b264-4d8fc3b6b11c)]
+interface nsIProcessToolsService: nsISupports {
+ /**
+ * Kill a process running on this system.
+ *
+ * Does not cause a crash report to be generated and sent.
+ *
+ * # Note
+ *
+ * `pid` is the unique-to-the-system process identifier, as
+ * obtained with attribute `pid` of this service.
+ *
+ * Under Un*ix, that's what you obtain with `getpid()`, etc.
+ * Under Windows, that's what you obtain with `GetCurrentProcessId()`,
+ * NOT the same thing as the process `HANDLE`.
+ *
+ * # Failure
+ *
+ * Under Windows, if two processes race to `kill()` a third process,
+ * or two threads race to `kill()` a process there is a (small) window
+ * during which this can cause a crash in the losing process.
+ *
+ * # Caveats
+ *
+ * Under Windows, process killing is asynchronous. Therefore, this
+ * function can return before process `pid` is actually dead.
+ */
+ void kill(in unsigned long long pid);
+
+ /**
+ * Crash a process running on this system. Generates a SIGABRT on Linux and
+ * macOS, and a STATUS_ILLEGAL_INSTRUCTION on Windows.
+ *
+ * Does cause a crash report to be generated.
+ */
+ void crash(in unsigned long long pid);
+
+ /**
+ * The pid for the current process.
+ */
+ readonly attribute unsigned long long pid;
+};
diff --git a/toolkit/components/processtools/src/lib.rs b/toolkit/components/processtools/src/lib.rs
new file mode 100644
index 0000000000..16bf2f8f47
--- /dev/null
+++ b/toolkit/components/processtools/src/lib.rs
@@ -0,0 +1,197 @@
+/* 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/. */
+
+#[cfg(not(target_os = "windows"))]
+extern crate libc;
+#[cfg(target_os = "windows")]
+extern crate winapi;
+
+extern crate nserror;
+extern crate xpcom;
+
+use std::convert::TryInto;
+
+use nserror::{nsresult, NS_ERROR_FAILURE, NS_OK};
+use xpcom::{interfaces::nsIProcessToolsService, xpcom, xpcom_method, RefPtr};
+
+// Separate this `use` to avoid build-breaking warnings.
+#[cfg(target_os = "windows")]
+use nserror::NS_ERROR_NOT_AVAILABLE;
+
+#[cfg(target_os = "windows")]
+struct Handle(winapi::um::winnt::HANDLE);
+
+#[cfg(target_os = "windows")]
+impl Handle {
+ fn from_raw(raw: winapi::um::winnt::HANDLE) -> Option<Self> {
+ (raw != std::ptr::null_mut() && raw != winapi::um::handleapi::INVALID_HANDLE_VALUE)
+ .then_some(Handle(raw))
+ }
+
+ fn raw(self: &Self) -> winapi::um::winnt::HANDLE {
+ self.0
+ }
+}
+
+#[cfg(target_os = "windows")]
+impl Drop for Handle {
+ fn drop(&mut self) {
+ unsafe {
+ winapi::um::handleapi::CloseHandle(self.raw());
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn new_process_tools_service(result: *mut *const nsIProcessToolsService) {
+ let service: RefPtr<ProcessToolsService> = ProcessToolsService::new();
+ RefPtr::new(service.coerce::<nsIProcessToolsService>()).forget(&mut *result);
+}
+
+// Implementation note:
+//
+// We're following the strategy employed by the `kvstore`.
+// See https://searchfox.org/mozilla-central/rev/a87a1c3b543475276e6d57a7a80cb02f3e42b6ed/toolkit/components/kvstore/src/lib.rs#78
+
+#[xpcom(implement(nsIProcessToolsService), atomic)]
+pub struct ProcessToolsService {}
+
+impl ProcessToolsService {
+ pub fn new() -> RefPtr<ProcessToolsService> {
+ ProcessToolsService::allocate(InitProcessToolsService {})
+ }
+
+ // Method `kill`.
+
+ xpcom_method!(
+ kill => Kill(id: u64)
+ );
+
+ #[cfg(target_os = "windows")]
+ pub fn kill(&self, pid: u64) -> Result<(), nsresult> {
+ let handle = unsafe {
+ winapi::um::processthreadsapi::OpenProcess(
+ /* dwDesiredAccess */
+ winapi::um::winnt::PROCESS_TERMINATE | winapi::um::winnt::SYNCHRONIZE,
+ /* bInheritHandle */ 0,
+ /* dwProcessId */ pid.try_into().unwrap(),
+ )
+ };
+ let handle = Handle::from_raw(handle).ok_or(NS_ERROR_NOT_AVAILABLE)?;
+
+ let result = unsafe {
+ winapi::um::processthreadsapi::TerminateProcess(
+ /* hProcess */ handle.raw(),
+ /* uExitCode */ 0,
+ )
+ };
+
+ if result == 0 {
+ return Err(NS_ERROR_FAILURE);
+ }
+ Ok(())
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn kill(&self, pid: u64) -> Result<(), nsresult> {
+ let pid = pid.try_into().or(Err(NS_ERROR_FAILURE))?;
+ let result = unsafe { libc::kill(pid, libc::SIGKILL) };
+ if result == 0 {
+ Ok(())
+ } else {
+ Err(NS_ERROR_FAILURE)
+ }
+ }
+
+ // Method `crash`
+
+ xpcom_method!(
+ crash => Crash(id: u64)
+ );
+
+ #[cfg(target_os = "windows")]
+ pub fn crash(&self, pid: u64) -> Result<(), nsresult> {
+ let ntdll = unsafe {
+ winapi::um::libloaderapi::GetModuleHandleA(
+ /* lpModuleName */ std::mem::transmute(b"ntdll.dll\0".as_ptr()),
+ )
+ };
+ if ntdll.is_null() {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ let dbg_break_point = unsafe {
+ winapi::um::libloaderapi::GetProcAddress(
+ /* hModule */ ntdll,
+ /* lpProcName */ std::mem::transmute(b"DbgBreakPoint\0".as_ptr()),
+ )
+ };
+ if dbg_break_point.is_null() {
+ return Err(NS_ERROR_NOT_AVAILABLE);
+ }
+
+ let target_proc = unsafe {
+ winapi::um::processthreadsapi::OpenProcess(
+ /* dwDesiredAccess */
+ winapi::um::winnt::PROCESS_VM_OPERATION
+ | winapi::um::winnt::PROCESS_CREATE_THREAD
+ | winapi::um::winnt::PROCESS_QUERY_INFORMATION,
+ /* bInheritHandle */ 0,
+ /* dwProcessId */ pid.try_into().unwrap(),
+ )
+ };
+ let target_proc = Handle::from_raw(target_proc).ok_or(NS_ERROR_NOT_AVAILABLE)?;
+
+ let new_thread = unsafe {
+ winapi::um::processthreadsapi::CreateRemoteThread(
+ /* hProcess */ target_proc.raw(),
+ /* lpThreadAttributes */ std::ptr::null_mut(),
+ /* dwStackSize */ 0,
+ /* lpStartAddress */ Some(std::mem::transmute(dbg_break_point)),
+ /* lpParameter */ std::ptr::null_mut(),
+ /* dwCreationFlags */ 0,
+ /* lpThreadId */ std::ptr::null_mut(),
+ )
+ };
+ let new_thread = Handle::from_raw(new_thread).ok_or(NS_ERROR_FAILURE)?;
+
+ unsafe {
+ winapi::um::synchapi::WaitForSingleObject(
+ new_thread.raw(),
+ winapi::um::winbase::INFINITE,
+ );
+ }
+
+ Ok(())
+ }
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn crash(&self, pid: u64) -> Result<(), nsresult> {
+ let pid = pid.try_into().or(Err(NS_ERROR_FAILURE))?;
+ let result = unsafe { libc::kill(pid, libc::SIGABRT) };
+ if result == 0 {
+ Ok(())
+ } else {
+ Err(NS_ERROR_FAILURE)
+ }
+ }
+
+ // Attribute `pid`
+
+ xpcom_method!(
+ get_pid => GetPid() -> u64
+ );
+
+ #[cfg(not(target_os = "windows"))]
+ pub fn get_pid(&self) -> Result<u64, nsresult> {
+ let pid = unsafe { libc::getpid() } as u64;
+ Ok(pid)
+ }
+
+ #[cfg(target_os = "windows")]
+ pub fn get_pid(&self) -> Result<u64, nsresult> {
+ let pid = unsafe { winapi::um::processthreadsapi::GetCurrentProcessId() } as u64;
+ Ok(pid)
+ }
+}
diff --git a/toolkit/components/processtools/tests/browser/browser.toml b/toolkit/components/processtools/tests/browser/browser.toml
new file mode 100644
index 0000000000..fa3655af4b
--- /dev/null
+++ b/toolkit/components/processtools/tests/browser/browser.toml
@@ -0,0 +1,11 @@
+[DEFAULT]
+prefs = ["media.rdd-process.enabled=true"]
+
+support-files = ["dummy.html"]
+
+["browser_test_powerMetrics.js"]
+
+["browser_test_procinfo.js"]
+skip-if = [
+ "ccov && os == 'linux'", # Bug 1608080
+]
diff --git a/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js b/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js
new file mode 100644
index 0000000000..93833a5207
--- /dev/null
+++ b/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js
@@ -0,0 +1,417 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ */
+function GetTestWebBasedURL(fileName) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+ ) + fileName
+ );
+}
+
+const kNS_PER_MS = 1000000;
+
+function printProcInfo(procInfo) {
+ info(
+ ` pid: ${procInfo.pid}, type = parent, cpu time = ${
+ procInfo.cpuTime / kNS_PER_MS
+ }ms`
+ );
+ for (let child of procInfo.children) {
+ info(
+ ` pid: ${child.pid}, type = ${child.type}, cpu time = ${
+ child.cpuTime / kNS_PER_MS
+ }ms`
+ );
+ }
+}
+
+// It would be nice to have an API to list all the statically defined labels of
+// a labeled_counter. Hopefully bug 1672273 will help with this.
+const kGleanProcessTypeLabels = [
+ "parent.active",
+ "parent.active.playing-audio",
+ "parent.active.playing-video",
+ "parent.inactive",
+ "parent.inactive.playing-audio",
+ "parent.inactive.playing-video",
+ "prealloc",
+ "privilegedabout",
+ "rdd",
+ "socket",
+ "web.background",
+ "web.background-perceivable",
+ "web.foreground",
+ "extension",
+ "gpu",
+ "gmplugin",
+ "utility",
+ "__other__",
+];
+
+async function getChildCpuTime(pid) {
+ return (await ChromeUtils.requestProcInfo()).children.find(p => p.pid == pid)
+ .cpuTime;
+}
+
+add_task(async () => {
+ // Temporarily open a new foreground tab to make the current tab a background
+ // tab, and burn some CPU time in it while it's in the background.
+ const kBusyWaitForMs = 50;
+ let cpuTimeSpentOnBackgroundTab;
+ let firstBrowser = gBrowser.selectedTab.linkedBrowser;
+ let processPriorityChangedPromise = BrowserTestUtils.contentTopicObserved(
+ firstBrowser.browsingContext,
+ "ipc:process-priority-changed"
+ );
+ await BrowserTestUtils.withNewTab(
+ GetTestWebBasedURL("dummy.html"),
+ async () => {
+ await processPriorityChangedPromise;
+ // We can't be sure that a busy loop lasting for a specific duration
+ // will use the same amount of CPU time, as that would require a core
+ // to be fully available for our busy loop, which is unlikely on single
+ // core hardware.
+ // To be able to have a predictable amount of CPU time used, we need to
+ // check using ChromeUtils.requestProcInfo how much CPU time has actually
+ // been spent.
+ let pid = firstBrowser.frameLoader.remoteTab.osPid;
+ let initalCpuTime = await getChildCpuTime(pid);
+ let afterCpuTime;
+ do {
+ await SpecialPowers.spawn(
+ firstBrowser,
+ [kBusyWaitForMs],
+ async kBusyWaitForMs => {
+ let startTime = Date.now();
+ while (Date.now() - startTime < 10) {
+ // Burn CPU time...
+ }
+ }
+ );
+ afterCpuTime = await getChildCpuTime(pid);
+ } while (afterCpuTime - initalCpuTime < kBusyWaitForMs * kNS_PER_MS);
+ cpuTimeSpentOnBackgroundTab = Math.floor(
+ (afterCpuTime - initalCpuTime) / kNS_PER_MS
+ );
+ }
+ );
+
+ let beforeProcInfo = await ChromeUtils.requestProcInfo();
+ await Services.fog.testFlushAllChildren();
+
+ let cpuTimeByType = {},
+ gpuTimeByType = {};
+ for (let label of kGleanProcessTypeLabels) {
+ cpuTimeByType[label] =
+ Glean.power.cpuTimePerProcessTypeMs[label].testGetValue();
+ gpuTimeByType[label] =
+ Glean.power.gpuTimePerProcessTypeMs[label].testGetValue();
+ }
+ let totalCpuTime = Glean.power.totalCpuTimeMs.testGetValue();
+ let totalGpuTime = Glean.power.totalGpuTimeMs.testGetValue();
+
+ let afterProcInfo = await ChromeUtils.requestProcInfo();
+
+ info("CPU time from ProcInfo before calling testFlushAllChildren:");
+ printProcInfo(beforeProcInfo);
+
+ info("CPU time for each label:");
+ let totalCpuTimeByType = 0;
+ for (let label of kGleanProcessTypeLabels) {
+ totalCpuTimeByType += cpuTimeByType[label] ?? 0;
+ info(` ${label} = ${cpuTimeByType[label]}`);
+ }
+
+ info("CPU time from ProcInfo after calling testFlushAllChildren:");
+ printProcInfo(afterProcInfo);
+
+ Assert.equal(
+ totalCpuTimeByType,
+ totalCpuTime,
+ "The sum of CPU time used by all process types should match totalCpuTimeMs"
+ );
+
+ // In infra the parent process time will be reported as parent.inactive,
+ // but when running the test locally the user might move the mouse a bit.
+ let parentTime =
+ (cpuTimeByType["parent.active"] || 0) +
+ (cpuTimeByType["parent.inactive"] || 0);
+ Assert.greaterOrEqual(
+ parentTime,
+ Math.floor(beforeProcInfo.cpuTime / kNS_PER_MS),
+ "reported parent cpu time should be at least what the first requestProcInfo returned"
+ );
+ Assert.lessOrEqual(
+ parentTime,
+ Math.ceil(afterProcInfo.cpuTime / kNS_PER_MS),
+ "reported parent cpu time should be at most what the second requestProcInfo returned"
+ );
+
+ kGleanProcessTypeLabels
+ .filter(label => label.startsWith("parent.") && label.includes(".playing-"))
+ .forEach(label => {
+ Assert.strictEqual(
+ cpuTimeByType[label],
+ null,
+ `no media was played so the CPU time for ${label} should be null`
+ );
+ });
+
+ if (beforeProcInfo.children.some(p => p.type == "preallocated")) {
+ Assert.greaterOrEqual(
+ cpuTimeByType.prealloc,
+ beforeProcInfo.children.reduce(
+ (time, p) =>
+ time +
+ (p.type == "preallocated" ? Math.floor(p.cpuTime / kNS_PER_MS) : 0),
+ 0
+ ),
+ "reported cpu time for preallocated content processes should be at least the sum of what the first requestProcInfo returned."
+ );
+ // We can't compare with the values returned by the second requestProcInfo
+ // call because one preallocated content processes has been turned into
+ // a normal content process when we opened a tab.
+ } else {
+ info(
+ "no preallocated content process existed when we started our test, but some might have existed before"
+ );
+ }
+
+ if (beforeProcInfo.children.some(p => p.type == "privilegedabout")) {
+ Assert.greaterOrEqual(
+ cpuTimeByType.privilegedabout,
+ 1,
+ "we used some CPU time in a foreground tab, but don't know how much as the process might have started as preallocated"
+ );
+ }
+
+ for (let label of [
+ "rdd",
+ "socket",
+ "extension",
+ "gpu",
+ "gmplugin",
+ "utility",
+ ]) {
+ if (!kGleanProcessTypeLabels.includes(label)) {
+ Assert.ok(
+ false,
+ `coding error in the test, ${label} isn't part of ${kGleanProcessTypeLabels.join(
+ ", "
+ )}`
+ );
+ }
+ if (beforeProcInfo.children.some(p => p.type == label)) {
+ function getCpuTime(procInfo) {
+ return (
+ procInfo.children.find(p => p.type == label).cpuTime / kNS_PER_MS
+ );
+ }
+ let beforeCpuTime = Math.floor(getCpuTime(beforeProcInfo));
+ let afterCpuTime = Math.ceil(getCpuTime(afterProcInfo));
+ let cpuTime = cpuTimeByType[label];
+ if (afterCpuTime == 0) {
+ Assert.equal(
+ cpuTime,
+ null,
+ "The " + label + " process used less than 1ms of CPU time."
+ );
+ } else if (beforeCpuTime == 0 && cpuTime === null) {
+ info(
+ "The " +
+ label +
+ " process might have used used less than 1ms of CPU time."
+ );
+ } else {
+ Assert.greaterOrEqual(
+ cpuTime,
+ beforeCpuTime,
+ "reported cpu time for " +
+ label +
+ " process should be at least what the first requestProcInfo returned."
+ );
+ Assert.lessOrEqual(
+ cpuTime,
+ afterCpuTime,
+ "reported cpu time for " +
+ label +
+ " process should be at most what the second requestProcInfo returned."
+ );
+ }
+ } else {
+ info(
+ "no " +
+ label +
+ " process existed when we started our test, but some might have existed before"
+ );
+ }
+ }
+
+ Assert.greaterOrEqual(
+ cpuTimeByType["web.background"],
+ cpuTimeSpentOnBackgroundTab,
+ "web.background should be at least the time we spent."
+ );
+
+ Assert.greaterOrEqual(
+ cpuTimeByType["web.foreground"],
+ 1,
+ "we used some CPU time in a foreground tab, but don't know how much"
+ );
+
+ // We only have web.background-perceivable CPU time if a muted video was
+ // played in a background tab.
+ Assert.strictEqual(
+ cpuTimeByType["web.background-perceivable"],
+ null,
+ "CPU time should only be recorded in the web.background-perceivable label"
+ );
+
+ // __other__ should be null, if it is not, we have a missing label in the metrics.yaml file.
+ Assert.strictEqual(
+ cpuTimeByType.__other__,
+ null,
+ "no CPU time should be recorded in the __other__ label"
+ );
+
+ info("GPU time for each label:");
+ let totalGpuTimeByType = undefined;
+ for (let label of kGleanProcessTypeLabels) {
+ if (gpuTimeByType[label] !== null) {
+ totalGpuTimeByType = (totalGpuTimeByType || 0) + gpuTimeByType[label];
+ }
+ info(` ${label} = ${gpuTimeByType[label]}`);
+ }
+
+ Assert.equal(
+ totalGpuTimeByType,
+ totalGpuTime,
+ "The sum of GPU time used by all process types should match totalGpuTimeMs"
+ );
+
+ // __other__ should be null, if it is not, we have a missing label in the metrics.yaml file.
+ Assert.strictEqual(
+ gpuTimeByType.__other__,
+ null,
+ "no GPU time should be recorded in the __other__ label"
+ );
+
+ // Now test per-thread CPU time.
+ // We don't test parentActive as the user is not marked active on infra.
+ let processTypes = [
+ "parentInactive",
+ "contentBackground",
+ "contentForeground",
+ ];
+ if (beforeProcInfo.children.some(p => p.type == "gpu")) {
+ processTypes.push("gpuProcess");
+ }
+ // The list of accepted labels is not accessible to the JS code, so test only the main thread.
+ const kThreadName = "geckomain";
+ if (AppConstants.NIGHTLY_BUILD) {
+ for (let processType of processTypes) {
+ Assert.greater(
+ Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(),
+ 0,
+ `some CPU time should have been recorded for the ${processType} main thread`
+ );
+ Assert.greater(
+ Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(),
+ 0,
+ `some thread wake ups should have been recorded for the ${processType} main thread`
+ );
+ }
+ } else {
+ // We are not recording per thread CPU use outside of the Nightly channel.
+ for (let processType of processTypes) {
+ Assert.equal(
+ Glean.powerCpuMsPerThread[processType][kThreadName].testGetValue(),
+ null,
+ `no CPU time should have been recorded for the ${processType} main thread`
+ );
+ Assert.equal(
+ Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(),
+ null,
+ `no thread wake ups should have been recorded for the ${processType} main thread`
+ );
+ }
+ }
+});
+
+add_task(async function test_tracker_power() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.trackingprotection.enabled", false],
+ ["privacy.trackingprotection.annotate_channels", true],
+ ],
+ });
+ let initialValues = [];
+ for (let trackerType of [
+ "ad",
+ "analytics",
+ "cryptomining",
+ "fingerprinting",
+ "social",
+ "unknown",
+ ]) {
+ initialValues[trackerType] =
+ Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0;
+ }
+
+ await BrowserTestUtils.withNewTab(
+ GetTestWebBasedURL("dummy.html"),
+ async () => {
+ // Load a tracker in a subframe, as we only record CPU time used by third party trackers.
+ await SpecialPowers.spawn(
+ gBrowser.selectedTab.linkedBrowser,
+ [
+ GetTestWebBasedURL("dummy.html").replace(
+ "example.org",
+ "trackertest.org"
+ ),
+ ],
+ async frameUrl => {
+ let iframe = content.document.createElement("iframe");
+ iframe.setAttribute("src", frameUrl);
+ await new content.Promise(resolve => {
+ iframe.onload = resolve;
+ content.document.body.appendChild(iframe);
+ });
+ }
+ );
+ }
+ );
+
+ await Services.fog.testFlushAllChildren();
+
+ let unknownTrackerCPUTime =
+ Glean.power.cpuTimePerTrackerTypeMs.unknown.testGetValue() || 0;
+ Assert.greater(
+ unknownTrackerCPUTime,
+ initialValues.unknown,
+ "The CPU time of unknown trackers should have increased"
+ );
+
+ for (let trackerType of [
+ "ad",
+ "analytics",
+ "cryptomining",
+ "fingerprinting",
+ "social",
+ ]) {
+ Assert.equal(
+ Glean.power.cpuTimePerTrackerTypeMs[trackerType].testGetValue() || 0,
+ initialValues[trackerType],
+ `no new CPU time should have been recorded for ${trackerType} trackers`
+ );
+ }
+});
diff --git a/toolkit/components/processtools/tests/browser/browser_test_procinfo.js b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js
new file mode 100644
index 0000000000..07c7d5d2d0
--- /dev/null
+++ b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js
@@ -0,0 +1,166 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const DUMMY_URL =
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.com"
+ ) + "/dummy.html";
+
+const isFissionEnabled = SpecialPowers.useRemoteSubframes;
+
+const SAMPLE_SIZE = 10;
+const NS_PER_MS = 1000000;
+
+function checkProcessCpuTime(proc) {
+ Assert.greater(proc.cpuTime, 0, "Got some cpu time");
+ Assert.greater(proc.threads.length, 0, "Got some threads");
+ Assert.ok(
+ proc.threads.some(thread => thread.cpuTime > 0),
+ "Got some cpu time in the threads"
+ );
+
+ let cpuThreads = 0;
+ for (let thread of proc.threads) {
+ cpuThreads += Math.floor(thread.cpuTime / NS_PER_MS);
+ }
+ // Add 1ms to the process CPU time because ProcInfo captures the CPU time for
+ // the whole process first and then for each of the threads, so the process
+ // CPU time might have increased slightly in the meantime.
+ let processCpuTime = Math.floor(proc.cpuTime / NS_PER_MS) + 1;
+ if (AppConstants.platform == "win" && processCpuTime < cpuThreads) {
+ // On Windows, our test jobs likely run in VMs without constant TSC,
+ // so we might have low precision CPU time measurements.
+ const MAX_DISCREPENCY = 100;
+ Assert.ok(
+ cpuThreads - processCpuTime < MAX_DISCREPENCY,
+ `on Windows, we accept a discrepency of up to ${MAX_DISCREPENCY}ms between the process CPU time and the sum of its threads' CPU time, process CPU time: ${processCpuTime}, sum of thread CPU time: ${cpuThreads}`
+ );
+ } else {
+ Assert.greaterOrEqual(
+ processCpuTime,
+ cpuThreads,
+ "The total CPU time of the process should be at least the sum of the CPU time spent by the still alive threads"
+ );
+ }
+}
+
+add_task(async function test_proc_info() {
+ // Open a few `about:home` tabs, they'll end up in `privilegedabout`.
+ let tabsAboutHome = [];
+ for (let i = 0; i < 5; ++i) {
+ let tab = BrowserTestUtils.addTab(gBrowser, "about:home");
+ tabsAboutHome.push(tab);
+ gBrowser.selectedTab = tab;
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ }
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: DUMMY_URL },
+ async function (browser) {
+ // We test `SAMPLE_SIZE` times to increase a tad the chance of encountering race conditions.
+ for (let z = 0; z < SAMPLE_SIZE; z++) {
+ let parentProc = await ChromeUtils.requestProcInfo();
+
+ Assert.equal(
+ parentProc.type,
+ "browser",
+ "Parent proc type should be browser"
+ );
+
+ checkProcessCpuTime(parentProc);
+
+ Assert.ok(
+ parentProc.threads.some(thread => thread.name),
+ "At least one of the threads of the parent process is named"
+ );
+
+ Assert.ok(parentProc.memory > 0, "Memory was set");
+
+ // While it's very unlikely that the parent will disappear while we're running
+ // tests, some children can easily vanish. So we go twice through the list of
+ // children. Once to test stuff that all process data respects the invariants
+ // that don't care whether we have a race condition and once to test that at
+ // least one well-known process that should not be able to vanish during
+ // the test respects all the invariants.
+ for (let childProc of parentProc.children) {
+ Assert.notEqual(
+ childProc.type,
+ "browser",
+ "Child proc type should not be browser"
+ );
+
+ // We set the `childID` for child processes that have a `ContentParent`/`ContentChild`
+ // actor hierarchy.
+ if (childProc.type.startsWith("web")) {
+ Assert.notEqual(
+ childProc.childID,
+ 0,
+ "Child proc should have been set"
+ );
+ }
+ Assert.notEqual(
+ childProc.type,
+ "unknown",
+ "Child proc type should be known"
+ );
+ if (childProc.type == "webIsolated") {
+ Assert.notEqual(
+ childProc.origin || "",
+ "",
+ "Child process should have an origin"
+ );
+ }
+
+ checkProcessCpuTime(childProc);
+ }
+
+ // We only check other properties on the `privilegedabout` subprocess, which
+ // as of this writing is always active and available.
+ var hasPrivilegedAbout = false;
+ var numberOfAboutTabs = 0;
+ for (let childProc of parentProc.children) {
+ if (childProc.type != "privilegedabout") {
+ continue;
+ }
+ hasPrivilegedAbout = true;
+ Assert.ok(childProc.memory > 0, "Memory was set");
+
+ for (var win of childProc.windows) {
+ if (win.documentURI.spec != "about:home") {
+ // We're only interested in about:home for this test.
+ continue;
+ }
+ numberOfAboutTabs++;
+ Assert.ok(
+ win.outerWindowId > 0,
+ `ContentParentID should be > 0 ${win.outerWindowId}`
+ );
+ if (win.documentTitle) {
+ // Unfortunately, we sometimes reach this point before the document is fully loaded, so
+ // `win.documentTitle` may still be empty.
+ Assert.equal(win.documentTitle, "New Tab");
+ }
+ }
+ Assert.ok(
+ numberOfAboutTabs >= tabsAboutHome.length,
+ "We have found at least as many about:home tabs as we opened"
+ );
+
+ // Once we have verified the privileged about process, bailout.
+ break;
+ }
+
+ Assert.ok(
+ hasPrivilegedAbout,
+ "We have found the privileged about process"
+ );
+ }
+
+ for (let tab of tabsAboutHome) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ }
+ );
+});
diff --git a/toolkit/components/processtools/tests/browser/dummy.html b/toolkit/components/processtools/tests/browser/dummy.html
new file mode 100644
index 0000000000..77752352d4
--- /dev/null
+++ b/toolkit/components/processtools/tests/browser/dummy.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+<title>Dummy test page</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Dummy test page</p>
+<div id="holder" class="">Holder</div>
+<script>
+ let text = "";
+ for (let i = 0; i < 1000; i++) {
+ text += "more";
+ document.getElementById("holder").innerHTML = text;
+ }
+ document.getElementById("holder").classList.add("loaded");
+</script>
+</body>
+</html>
diff --git a/toolkit/components/processtools/tests/xpcshell/test_process_kill.js b/toolkit/components/processtools/tests/xpcshell/test_process_kill.js
new file mode 100644
index 0000000000..9781efaffc
--- /dev/null
+++ b/toolkit/components/processtools/tests/xpcshell/test_process_kill.js
@@ -0,0 +1,52 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const { Subprocess } = ChromeUtils.importESModule(
+ "resource://gre/modules/Subprocess.sys.mjs"
+);
+
+const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+);
+
+let PYTHON;
+
+// Find Python.
+add_task(async function setup() {
+ PYTHON = await Subprocess.pathSearch(Services.env.get("PYTHON"));
+});
+
+// Ensure that killing a process... kills the process.
+add_task(async function test_subprocess_kill() {
+ // We launch Python, as it's a long-running process and it exists
+ // on all desktop platforms on which we run tests.
+ let proc = await Subprocess.call({
+ command: PYTHON,
+ arguments: [],
+ });
+
+ let isTerminated = false;
+
+ proc.wait().then(() => {
+ isTerminated = true;
+ });
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+ Assert.ok(
+ !isTerminated,
+ "We haven't killed the process yet, it should still be running."
+ );
+
+ // Time to kill the process.
+ ProcessTools.kill(proc.pid);
+
+ await new Promise(resolve => setTimeout(resolve, 100));
+ Assert.ok(
+ isTerminated,
+ "We have killed the process already, it shouldn't be running anymore."
+ );
+});
diff --git a/toolkit/components/processtools/tests/xpcshell/test_total_cpu_time.js b/toolkit/components/processtools/tests/xpcshell/test_total_cpu_time.js
new file mode 100644
index 0000000000..00f1ea2b6c
--- /dev/null
+++ b/toolkit/components/processtools/tests/xpcshell/test_total_cpu_time.js
@@ -0,0 +1,122 @@
+"use strict";
+
+var cpuThreadCount;
+
+add_task(async function setup() {
+ // FOG needs a profile directory to put its data in.
+ do_get_profile();
+
+ Services.fog.initializeFOG();
+
+ cpuThreadCount = (await Services.sysinfo.processInfo).count;
+});
+
+async function getCpuTimeFromProcInfo() {
+ const NS_PER_MS = 1000000;
+ let cpuTimeForProcess = p => p.cpuTime / NS_PER_MS;
+ let procInfo = await ChromeUtils.requestProcInfo();
+ return (
+ cpuTimeForProcess(procInfo) +
+ procInfo.children.map(cpuTimeForProcess).reduce((a, b) => a + b, 0)
+ );
+}
+
+add_task(async function test_totalCpuTime_in_parent() {
+ let startTime = Date.now();
+
+ let initialProcInfoCpuTime = Math.floor(await getCpuTimeFromProcInfo());
+ await Services.fog.testFlushAllChildren();
+
+ let initialCpuTime = Glean.power.totalCpuTimeMs.testGetValue();
+ Assert.greater(
+ initialCpuTime,
+ 0,
+ "The CPU time used by starting the test harness should be more than 0"
+ );
+ let initialProcInfoCpuTime2 = Math.ceil(await getCpuTimeFromProcInfo());
+
+ Assert.greaterOrEqual(
+ initialCpuTime,
+ initialProcInfoCpuTime,
+ "The CPU time reported through Glean should be at least as much as the CPU time reported by ProcInfo before asking Glean for the data"
+ );
+ Assert.lessOrEqual(
+ initialCpuTime,
+ initialProcInfoCpuTime2,
+ "The CPU time reported through Glean should be no more than the CPU time reported by ProcInfo after asking Glean for the data"
+ );
+
+ // 50 is an arbitrary value, but the resolution is 16ms on Windows,
+ // so this needs to be significantly more than 16.
+ const kBusyWaitForMs = 50;
+ while (Date.now() - startTime < kBusyWaitForMs) {
+ // Burn CPU time...
+ }
+
+ let additionalProcInfoCpuTime =
+ Math.floor(await getCpuTimeFromProcInfo()) - initialProcInfoCpuTime2;
+ await Services.fog.testFlushAllChildren();
+
+ let additionalCpuTime =
+ Glean.power.totalCpuTimeMs.testGetValue() - initialCpuTime;
+ info(
+ `additional CPU time according to ProcInfo: ${additionalProcInfoCpuTime}ms and Glean ${additionalCpuTime}ms`
+ );
+
+ // On a machine where the CPU is very busy, our busy wait loop might burn less
+ // CPU than expected if other processes are being scheduled instead of us.
+ let expectedAdditionalCpuTime = Math.min(
+ additionalProcInfoCpuTime,
+ kBusyWaitForMs
+ );
+ Assert.greaterOrEqual(
+ additionalCpuTime,
+ expectedAdditionalCpuTime,
+ `The total CPU time should have increased by at least ${expectedAdditionalCpuTime}ms`
+ );
+ let wallClockTime = Date.now() - startTime;
+ Assert.lessOrEqual(
+ additionalCpuTime,
+ wallClockTime * cpuThreadCount,
+ `The total CPU time should have increased by at most the wall clock time (${wallClockTime}ms) * ${cpuThreadCount} CPU threads`
+ );
+});
+
+add_task(async function test_totalCpuTime_in_child() {
+ const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+ let startTime = Date.now();
+ await Services.fog.testFlushAllChildren();
+ let initialCpuTime = Glean.power.totalCpuTimeMs.testGetValue();
+
+ let initialProcInfoCpuTime = await getCpuTimeFromProcInfo();
+ run_test_in_child("test_total_cpu_time_child.js");
+ let burntCpuTime = await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+ let additionalProcInfoCpuTime =
+ (await getCpuTimeFromProcInfo()) - initialProcInfoCpuTime;
+
+ await Services.fog.testFlushAllChildren();
+ let additionalCpuTime =
+ Glean.power.totalCpuTimeMs.testGetValue() - initialCpuTime;
+ info(
+ `additional CPU time according to ProcInfo: ${additionalProcInfoCpuTime}ms and Glean ${additionalCpuTime}ms`
+ );
+
+ // On a machine where the CPU is very busy, our busy wait loop might burn less
+ // CPU than expected if other processes are being scheduled instead of us.
+ let expectedAdditionalCpuTime = Math.min(
+ Math.floor(additionalProcInfoCpuTime),
+ burntCpuTime
+ );
+ Assert.greaterOrEqual(
+ additionalCpuTime,
+ expectedAdditionalCpuTime,
+ `The total CPU time should have increased by at least ${expectedAdditionalCpuTime}ms`
+ );
+ let wallClockTime = Date.now() - startTime;
+ Assert.lessOrEqual(
+ additionalCpuTime,
+ wallClockTime * cpuThreadCount,
+ `The total CPU time should have increased by at most the wall clock time (${wallClockTime}ms) * ${cpuThreadCount} CPU threads`
+ );
+});
diff --git a/toolkit/components/processtools/tests/xpcshell/test_total_cpu_time_child.js b/toolkit/components/processtools/tests/xpcshell/test_total_cpu_time_child.js
new file mode 100644
index 0000000000..88ac480a09
--- /dev/null
+++ b/toolkit/components/processtools/tests/xpcshell/test_total_cpu_time_child.js
@@ -0,0 +1,10 @@
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+function run_test() {
+ const kBusyWaitForMs = 50;
+ let startTime = Date.now();
+ while (Date.now() - startTime < kBusyWaitForMs) {
+ // Burn CPU time...
+ }
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE, kBusyWaitForMs);
+}
diff --git a/toolkit/components/processtools/tests/xpcshell/xpcshell.toml b/toolkit/components/processtools/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..e61f779ee1
--- /dev/null
+++ b/toolkit/components/processtools/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,10 @@
+[DEFAULT]
+firefox-appdir = "browser"
+subprocess = true
+
+["test_process_kill.js"]
+skip-if = ["os == 'android'"]
+
+["test_total_cpu_time.js"]
+skip-if = ["socketprocess_networking"]
+support-files = ["test_total_cpu_time_child.js"]