Bug 1665318 - reduce the overhead of collecting memory information for about:processes, r=dthayer.

Differential Revision: https://phabricator.services.mozilla.com/D125729
This commit is contained in:
Florian Quèze 2021-09-16 19:59:56 +00:00
parent 4fcdfbc981
commit 3ba2c38074
8 changed files with 158 additions and 210 deletions

View File

@ -126,7 +126,7 @@ let DefaultTabUnloaderMethods = {
processInfo = { count: 0, topCount: 0, tabSet: new Set() };
processMap.set(childProcInfo.pid, processInfo);
}
processInfo.memory = childProcInfo.residentUniqueSize;
processInfo.memory = childProcInfo.memory;
}
},
};

View File

@ -672,15 +672,11 @@ dictionary ChildProcInfoDictionary {
// Process filename (without the path name).
DOMString filename = "";
// RSS, in bytes, i.e. the total amount of memory allocated
// by this process.
long long residentSetSize = 0;
// Resident unique size, i.e. the total amount of memory
// allocated by this process *and not shared with other processes*.
// Given that we share lots of memory between processes,
// this is probably the best end-user measure for "memory used".
long long residentUniqueSize = 0;
// The best end-user measure for "memory used" that we can obtain without
// triggering expensive computations. The value is in bytes.
// On Mac and Linux this matches the values shown by the system monitors.
// On Windows this will return the Commit Size.
unsigned long long memory = 0;
// Time spent by the process in user mode, in ns.
unsigned long long cpuUser = 0;
@ -719,15 +715,11 @@ dictionary ParentProcInfoDictionary {
// Process filename (without the path name).
DOMString filename = "";
// RSS, in bytes, i.e. the total amount of memory allocated
// by this process.
long long residentSetSize = 0;
// Resident unique size, i.e. the total amount of memory
// allocated by this process *and not shared with other processes*.
// Given that we share lots of memory between processes,
// this is probably the best end-user measure for "memory used".
long long residentUniqueSize = 0;
// The best end-user measure for "memory used" that we can obtain without
// triggering expensive computations. The value is in bytes.
// On Mac and Linux this matches the values shown by the system monitors.
// On Windows this will return the Commit Size.
unsigned long long memory = 0;
// Time spent by the process in user mode, in ns.
unsigned long long cpuUser = 0;

View File

@ -308,18 +308,11 @@ var State = {
*/
_getProcessDelta(cur, prev) {
let windows = this._getDOMWindows(cur);
// Resident set size is the total memory used by the process, including shared memory.
// Resident unique size is the memory used by the process, without shared memory.
// Since all processes share memory with the parent process, we count the shared memory
// as part of the parent process (`"browser"`) rather than as part of the individual
// processes.
let totalRamSize =
cur.type == "browser" ? cur.residentSetSize : cur.residentUniqueSize;
let result = {
pid: cur.pid,
childID: cur.childID,
filename: cur.filename,
totalRamSize,
totalRamSize: cur.memory,
deltaRamSize: null,
totalCpuUser: cur.cpuUser,
slopeCpuUser: null,
@ -372,10 +365,7 @@ var State = {
return this._getThreadDelta(curThread, prevThread, deltaT);
});
}
result.deltaRamSize =
cur.type == "browser"
? cur.residentSetSize - prev.residentSetSize
: cur.residentUniqueSize - prev.residentUniqueSize;
result.deltaRamSize = cur.memory - prev.memory;
result.slopeCpuUser = (cur.cpuUser - prev.cpuUser) / deltaT;
result.slopeCpuKernel = (cur.cpuKernel - prev.cpuKernel) / deltaT;
result.slopeCpu = result.slopeCpuUser + result.slopeCpuKernel;

View File

@ -105,10 +105,8 @@ struct ProcInfo {
nsCString origin;
// Process filename (without the path name).
nsString filename;
// RSS in bytes.
int64_t residentSetSize = 0;
// Unshared resident size in bytes.
int64_t residentUniqueSize = 0;
// Memory size in bytes.
uint64_t memory = 0;
// User time in ns.
uint64_t cpuUser = 0;
// System time in ns.
@ -212,8 +210,7 @@ nsresult CopySysProcInfoToDOM(const ProcInfo& source, T* dest) {
// Copy system info.
dest->mPid = source.pid;
dest->mFilename.Assign(source.filename);
dest->mResidentSetSize = source.residentSetSize;
dest->mResidentUniqueSize = source.residentUniqueSize;
dest->mMemory = source.memory;
dest->mCpuUser = source.cpuUser;
dest->mCpuKernel = source.cpuKernel;

View File

@ -32,127 +32,126 @@ RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
return promise;
}
RefPtr<nsIRunnable> r = NS_NewRunnableFunction(
__func__, [holder = std::move(holder), requests = std::move(aRequests)]() {
HashMap<base::ProcessId, ProcInfo> gathered;
if (!gathered.reserve(requests.Length())) {
RefPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [holder = std::move(holder),
requests = std::move(aRequests)]() {
HashMap<base::ProcessId, ProcInfo> gathered;
if (!gathered.reserve(requests.Length())) {
holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
return;
}
for (const auto& request : requests) {
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);
struct proc_bsdinfo proc;
if ((unsigned long)proc_pidinfo(request.pid, PROC_PIDTBSDINFO, 0, &proc,
PROC_PIDTBSDINFO_SIZE) < PROC_PIDTBSDINFO_SIZE) {
// 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;
}
struct proc_taskinfo pti;
if ((unsigned long)proc_pidinfo(request.pid, PROC_PIDTASKINFO, 0, &pti,
PROC_PIDTASKINFO_SIZE) < PROC_PIDTASKINFO_SIZE) {
continue;
}
// copying all the info to the ProcInfo struct
info.filename.AssignASCII(proc.pbi_name);
info.cpuUser = pti.pti_total_user;
info.cpuKernel = pti.pti_total_system;
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) {
holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
return;
}
for (const auto& request : requests) {
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);
struct proc_bsdinfo proc;
if ((unsigned long)proc_pidinfo(request.pid, PROC_PIDTBSDINFO, 0, &proc,
PROC_PIDTBSDINFO_SIZE) < PROC_PIDTBSDINFO_SIZE) {
// 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;
}
thread->cpuUser = threadInfoData.pth_user_time;
thread->cpuKernel = threadInfoData.pth_system_time;
thread->name.AssignASCII(threadInfoData.pth_name);
thread->tid = identifierInfo.thread_id;
}
struct proc_taskinfo pti;
if ((unsigned long)proc_pidinfo(request.pid, PROC_PIDTASKINFO, 0, &pti,
PROC_PIDTASKINFO_SIZE) < PROC_PIDTASKINFO_SIZE) {
continue;
}
// copying all the info to the ProcInfo struct
info.filename.AssignASCII(proc.pbi_name);
info.residentSetSize = pti.pti_resident_size;
info.cpuUser = pti.pti_total_user;
info.cpuKernel = pti.pti_total_system;
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;
}
// Computing the resident unique size is somewhat tricky,
// so we use about:memory's implementation. This implementation
// uses the `task` so, in theory, should be no additional
// race condition. However, in case of error, the result is `0`.
info.residentUniqueSize = nsMemoryReporterManager::ResidentUnique(selectedTask);
// 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);
});
mach_msg_type_number_t count;
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) {
holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
return;
}
thread->cpuUser = threadInfoData.pth_user_time;
thread->cpuKernel = threadInfoData.pth_system_time;
thread->name.AssignASCII(threadInfoData.pth_name);
thread->tid = identifierInfo.thread_id;
}
if (!gathered.put(request.pid, std::move(info))) {
holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
return;
}
}
// ... and we're done!
holder->Resolve(std::move(gathered), __func__);
});
if (!gathered.put(request.pid, std::move(info))) {
holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);
return;
}
}
// ... and we're done!
holder->Resolve(std::move(gathered), __func__);
});
rv = target->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
if (NS_FAILED(rv)) {

View File

@ -9,7 +9,6 @@
#include "mozilla/Logging.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/ipc/GeckoChildProcessHost.h"
#include "nsLocalFile.h"
#include "nsMemoryReporterManager.h"
#include "nsNetCID.h"
#include "nsWhitespaceTokenizer.h"
@ -38,9 +37,7 @@ namespace mozilla {
class StatReader {
public:
explicit StatReader(const base::ProcessId aPid)
: mPid(aPid), mMaxIndex(53), mTicksPerSec(sysconf(_SC_CLK_TCK)) {
mFilepath.AppendPrintf("/proc/%u/stat", mPid);
}
: mPid(aPid), mMaxIndex(15), mTicksPerSec(sysconf(_SC_CLK_TCK)) {}
nsresult ParseProc(ProcInfo& aInfo) {
nsAutoString fileContent;
@ -89,14 +86,6 @@ class StatReader {
aInfo.cpuKernel = GetCPUTime(aToken, &rv);
NS_ENSURE_SUCCESS(rv, rv);
break;
case 23:
// Resident Set Size: number of pages the process has
// in real memory.
uint64_t pageCount = Get64Value(aToken, &rv);
NS_ENSURE_SUCCESS(rv, rv);
uint64_t pageSize = sysconf(_SC_PAGESIZE);
aInfo.residentSetSize = pageCount * pageSize;
break;
}
return rv;
}
@ -136,21 +125,17 @@ class StatReader {
private:
// Reads the stat file and puts its content in a nsString.
nsresult ReadFile(nsAutoString& aFileContent) {
RefPtr<nsLocalFile> file = new nsLocalFile(mFilepath);
bool exists;
nsresult rv = file->Exists(&exists);
NS_ENSURE_SUCCESS(rv, rv);
if (!exists) {
if (mFilepath.IsEmpty()) {
mFilepath.AppendPrintf("/proc/%u/stat", 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.
FILE* fstat;
if (NS_FAILED(file->OpenANSIFileDesc("r", &fstat)) || !fstat) {
return NS_ERROR_FAILURE;
}
char buffer[2048];
char* end;
char* start = fgets(buffer, 2048, fstat);
@ -176,10 +161,7 @@ class ThreadInfoReader final : public StatReader {
public:
ThreadInfoReader(const base::ProcessId aPid, const base::ProcessId aTid)
: StatReader(aPid), mTid(aTid) {
// Adding the thread path
mFilepath.Truncate();
mFilepath.AppendPrintf("/proc/%u/task/%u/stat", aPid, mTid);
mMaxIndex = 17;
}
nsresult ParseThread(ThreadInfo& aInfo) {
@ -230,12 +212,22 @@ RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
// the process has been just been killed. Regardless, skip process.
continue;
}
// Computing the resident unique size is somewhat tricky,
// so we use about:memory's implementation. This implementation
// reopens `/proc/[pid]`, so there is the risk of an additional
// race condition. In that case, the result is `0`.
info.residentUniqueSize =
nsMemoryReporterManager::ResidentUnique(request.pid);
// 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;

View File

@ -68,7 +68,7 @@ RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
// Ignore process, it may have died.
continue;
}
PROCESS_MEMORY_COUNTERS memoryCounters;
PROCESS_MEMORY_COUNTERS_EX memoryCounters;
if (!GetProcessMemoryInfo(handle.get(),
(PPROCESS_MEMORY_COUNTERS)&memoryCounters,
sizeof(memoryCounters))) {
@ -91,14 +91,7 @@ RefPtr<ProcInfoPromise> GetProcInfo(nsTArray<ProcInfoRequest>&& aRequests) {
info.filename.Assign(filename);
info.cpuKernel = ToNanoSeconds(kernelTime);
info.cpuUser = ToNanoSeconds(userTime);
info.residentSetSize = memoryCounters.WorkingSetSize;
// Computing the resident unique size is somewhat tricky,
// so we use about:memory's implementation. This implementation
// uses the `HANDLE` so, in theory, should be no additional
// race condition. However, in case of error, the result is `0`.
info.residentUniqueSize =
nsMemoryReporterManager::ResidentUnique(handle.get());
info.memory = memoryCounters.PrivateUsage;
if (!gathered.put(request.pid, std::move(info))) {
holder->Reject(NS_ERROR_OUT_OF_MEMORY, __func__);

View File

@ -20,7 +20,6 @@ const isFissionEnabled = SpecialPowers.useRemoteSubframes;
const SAMPLE_SIZE = 10;
add_task(async function test_proc_info() {
console.log("YORIC", "Test starts");
// Open a few `about:home` tabs, they'll end up in `privilegedabout`.
let tabsAboutHome = [];
for (let i = 0; i < 5; ++i) {
@ -59,14 +58,7 @@ add_task(async function test_proc_info() {
);
}
Assert.ok(
parentProc.residentUniqueSize > 0,
"Resident-unique-size was set"
);
Assert.ok(
parentProc.residentUniqueSize <= parentProc.residentSetSize,
`Resident-unique-size should be bounded by resident-set-size ${parentProc.residentUniqueSize} <= ${parentProc.residentSetSize}`
);
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
@ -120,14 +112,7 @@ add_task(async function test_proc_info() {
continue;
}
hasPrivilegedAbout = true;
Assert.ok(
childProc.residentUniqueSize > 0,
"Resident-unique-size was set"
);
Assert.ok(
childProc.residentUniqueSize <= childProc.residentSetSize,
`Resident-unique-size should be bounded by resident-set-size ${childProc.residentUniqueSize} <= ${childProc.residentSetSize}`
);
Assert.ok(childProc.memory > 0, "Memory was set");
for (var win of childProc.windows) {
if (win.documentURI.spec != "about:home") {