diff options
Diffstat (limited to 'toolkit/components/processtools')
23 files changed, 2762 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..ce4bd1d9b2 --- /dev/null +++ b/toolkit/components/processtools/ProcInfo.h @@ -0,0 +1,282 @@ +/* -*- 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, +}; + +enum class UtilityActorName { + Unknown, + AudioDecoder_Generic, + AudioDecoder_AppleMedia, + AudioDecoder_WMF, + MfMediaEngineCDM, + JSOracle, +}; + +// String that will be used e.g. to annotate crash reports +nsCString GetUtilityActorName(const UtilityActorName aActorName); + +/* 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..80804aea42 --- /dev/null +++ b/toolkit/components/processtools/ProcInfo.mm @@ -0,0 +1,180 @@ +/* -*- 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..5a44775cb7 --- /dev/null +++ b/toolkit/components/processtools/ProcInfo_common.cpp @@ -0,0 +1,62 @@ +/* -*- 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; + } + 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_win.cpp b/toolkit/components/processtools/ProcInfo_win.cpp new file mode 100644 index 0000000000..f290f3d8df --- /dev/null +++ b/toolkit/components/processtools/ProcInfo_win.cpp @@ -0,0 +1,283 @@ +/* -*- 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> + +#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 GetCycleTimeFrequencyMHz() { + static const int frequency = []() { + // Having a constant TSC is required to convert cycle time to actual time. + if (!mozilla::has_constant_tsc()) { + return 0; + } + + // Now 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; +} + +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..b7c164c1b0 --- /dev/null +++ b/toolkit/components/processtools/moz.build @@ -0,0 +1,51 @@ +# -*- 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.ini"] +BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.ini"] + +# 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"] + 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..a84aa9ef62 --- /dev/null +++ b/toolkit/components/processtools/src/lib.rs @@ -0,0 +1,177 @@ +/* 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; + +#[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); +} + +#[cfg(target_os = "windows")] +use std::ffi::c_void; + +// We want to generate an exception that can be caught by the exception handler +// when injecting in a remote process, STATUS_ACCESS_VIOLATION seems not to do, +// it but the following code generates a STATUS_ILLEGAL_INSTRUCTION that seems +// to do the trick +#[cfg(target_os = "windows")] +pub unsafe extern "system" fn crash_illegal_instruction(_arg: *mut c_void) -> u32 { + std::ptr::null_mut::<u32>().write(1); + 0 +} + +// 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(), + ) + }; + if handle.is_null() { + // Could not open process. + return Err(NS_ERROR_NOT_AVAILABLE); + } + + let result = unsafe { + winapi::um::processthreadsapi::TerminateProcess( + /* hProcess */ handle, /* uExitCode */ 0, + ) + }; + + // Close handle regardless of success. + let _ = unsafe { winapi::um::handleapi::CloseHandle(handle) }; + + 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 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(), + ) + }; + if target_proc.is_null() { + // Could not open process. + return Err(NS_ERROR_NOT_AVAILABLE); + } + + let new_thread = unsafe { + winapi::um::processthreadsapi::CreateRemoteThread( + /* hProcess */ target_proc, + /* lpThreadAttributes */ std::ptr::null_mut(), + /* dwStackSize */ 0, + /* lpStartAddress */ Some(crash_illegal_instruction), + /* lpParameter */ std::ptr::null_mut(), + /* dwCreationFlags */ 0, + /* lpThreadId */ std::ptr::null_mut(), + ) + }; + + // Close handle regardless of success. + let _ = unsafe { + winapi::um::synchapi::WaitForSingleObject(new_thread, winapi::um::winbase::INFINITE); + winapi::um::handleapi::CloseHandle(new_thread); + winapi::um::handleapi::CloseHandle(target_proc); + }; + + if new_thread.is_null() { + return Err(NS_ERROR_FAILURE); + } + 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.ini b/toolkit/components/processtools/tests/browser/browser.ini new file mode 100644 index 0000000000..a7436faaf5 --- /dev/null +++ b/toolkit/components/processtools/tests/browser/browser.ini @@ -0,0 +1,10 @@ +[DEFAULT] +prefs= + media.rdd-process.enabled=true + +support-files = + dummy.html + +[browser_test_powerMetrics.js] +[browser_test_procinfo.js] +skip-if = (ccov && os == "linux") # https://bugzilla.mozilla.org/show_bug.cgi?id=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..8dfef0651d --- /dev/null +++ b/toolkit/components/processtools/tests/browser/browser_test_powerMetrics.js @@ -0,0 +1,400 @@ +/* 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], + undefined, + `no media was played so the CPU time for ${label} should be undefined` + ); + }); + + 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)) { + Assert.greaterOrEqual( + cpuTimeByType[label], + Math.floor( + beforeProcInfo.children.find(p => p.type == label).cpuTime / + kNS_PER_MS + ), + "reported cpu time for " + + label + + " process should be at least what the first requestProcInfo returned." + ); + Assert.lessOrEqual( + cpuTimeByType[label], + Math.ceil( + afterProcInfo.children.find(p => p.type == label).cpuTime / kNS_PER_MS + ), + "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"], + undefined, + "CPU time should only be recorded in the web.background-perceivable label" + ); + + // __other__ should be undefined, if it is not, we have a missing label in the metrics.yaml file. + Assert.strictEqual( + cpuTimeByType.__other__, + undefined, + "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] !== undefined) { + 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 undefined, if it is not, we have a missing label in the metrics.yaml file. + Assert.strictEqual( + gpuTimeByType.__other__, + undefined, + "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(), + undefined, + `no CPU time should have been recorded for the ${processType} main thread` + ); + Assert.equal( + Glean.powerWakeupsPerThread[processType][kThreadName].testGetValue(), + undefined, + `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..673864bdd0 --- /dev/null +++ b/toolkit/components/processtools/tests/browser/browser_test_procinfo.js @@ -0,0 +1,168 @@ +/* -*- 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 { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const HAS_THREAD_NAMES = + AppConstants.platform != "win" || + AppConstants.isPlatformAndVersionAtLeast("win", 10); +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"); + + let cpuThreads = 0; + for (let thread of proc.threads) { + cpuThreads += Math.floor(thread.cpuTime / NS_PER_MS); + } + Assert.greater(cpuThreads, 0, "Got some cpu time in the threads"); + let processCpuTime = Math.ceil(proc.cpuTime / NS_PER_MS); + 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); + + // Under Windows, thread names appeared with Windows 10. + if (HAS_THREAD_NAMES) { + 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..e69dad24d4 --- /dev/null +++ b/toolkit/components/processtools/tests/browser/dummy.html @@ -0,0 +1,20 @@ +<!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"; + // eslint-disable-next-line no-unsanitized/property + 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.ini b/toolkit/components/processtools/tests/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..bd9bdaac93 --- /dev/null +++ b/toolkit/components/processtools/tests/xpcshell/xpcshell.ini @@ -0,0 +1,9 @@ +[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 |