summaryrefslogtreecommitdiff
path: root/xpcom/base/SystemMemoryReporter.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xpcom/base/SystemMemoryReporter.cpp')
-rw-r--r--xpcom/base/SystemMemoryReporter.cpp989
1 files changed, 989 insertions, 0 deletions
diff --git a/xpcom/base/SystemMemoryReporter.cpp b/xpcom/base/SystemMemoryReporter.cpp
new file mode 100644
index 000000000..105b9c8cf
--- /dev/null
+++ b/xpcom/base/SystemMemoryReporter.cpp
@@ -0,0 +1,989 @@
+/* -*- 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/SystemMemoryReporter.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/LinuxUtils.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TaggedAnonymousMemory.h"
+#include "mozilla/Unused.h"
+
+#include "nsDataHashtable.h"
+#include "nsIMemoryReporter.h"
+#include "nsPrintfCString.h"
+#include "nsString.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+
+#include <dirent.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <errno.h>
+
+// This file implements a Linux-specific, system-wide memory reporter. It
+// gathers all the useful memory measurements obtainable from the OS in a
+// single place, giving a high-level view of memory consumption for the entire
+// machine/device.
+//
+// Other memory reporters measure part of a single process's memory consumption.
+// This reporter is different in that it measures memory consumption of many
+// processes, and they end up in a single reports tree. This is a slight abuse
+// of the memory reporting infrastructure, and therefore the results are given
+// their own "process" called "System", which means they show up in about:memory
+// in their own section, distinct from the per-process sections.
+
+namespace mozilla {
+namespace SystemMemoryReporter {
+
+#if !defined(XP_LINUX)
+#error "This won't work if we're not on Linux."
+#endif
+
+/**
+ * RAII helper that will close an open DIR handle.
+ */
+struct MOZ_STACK_CLASS AutoDir
+{
+ explicit AutoDir(DIR* aDir) : mDir(aDir) {}
+ ~AutoDir() { if (mDir) closedir(mDir); };
+ DIR* mDir;
+};
+
+/**
+ * RAII helper that will close an open FILE handle.
+ */
+struct MOZ_STACK_CLASS AutoFile
+{
+ explicit AutoFile(FILE* aFile) : mFile(aFile) {}
+ ~AutoFile() { if (mFile) fclose(mFile); }
+ FILE* mFile;
+};
+
+static bool
+EndsWithLiteral(const nsCString& aHaystack, const char* aNeedle)
+{
+ int32_t idx = aHaystack.RFind(aNeedle);
+ return idx != -1 && idx + strlen(aNeedle) == aHaystack.Length();
+}
+
+static void
+GetDirname(const nsCString& aPath, nsACString& aOut)
+{
+ int32_t idx = aPath.RFind("/");
+ if (idx == -1) {
+ aOut.Truncate();
+ } else {
+ aOut.Assign(Substring(aPath, 0, idx));
+ }
+}
+
+static void
+GetBasename(const nsCString& aPath, nsACString& aOut)
+{
+ nsCString out;
+ int32_t idx = aPath.RFind("/");
+ if (idx == -1) {
+ out.Assign(aPath);
+ } else {
+ out.Assign(Substring(aPath, idx + 1));
+ }
+
+ // On Android, some entries in /dev/ashmem end with "(deleted)" (e.g.
+ // "/dev/ashmem/libxul.so(deleted)"). We don't care about this modifier, so
+ // cut it off when getting the entry's basename.
+ if (EndsWithLiteral(out, "(deleted)")) {
+ out.Assign(Substring(out, 0, out.RFind("(deleted)")));
+ }
+ out.StripChars(" ");
+
+ aOut.Assign(out);
+}
+
+static bool
+IsNumeric(const char* aStr)
+{
+ MOZ_ASSERT(*aStr); // shouldn't see empty strings
+ while (*aStr) {
+ if (!isdigit(*aStr)) {
+ return false;
+ }
+ ++aStr;
+ }
+ return true;
+}
+
+static bool
+IsAnonymous(const nsACString& aName)
+{
+ // Recent kernels have multiple [stack:nnnn] entries, where |nnnn| is a
+ // thread ID. However, the entire virtual memory area containing a thread's
+ // stack pointer is considered the stack for that thread, even if it was
+ // merged with an adjacent area containing non-stack data. So we treat them
+ // as regular anonymous memory. However, see below about tagged anonymous
+ // memory.
+ return aName.IsEmpty() ||
+ StringBeginsWith(aName, NS_LITERAL_CSTRING("[stack:"));
+}
+
+class SystemReporter final : public nsIMemoryReporter
+{
+ ~SystemReporter() {}
+
+public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+#define REPORT(_path, _units, _amount, _desc) \
+ do { \
+ size_t __amount = _amount; /* evaluate _amount only once */ \
+ if (__amount > 0) { \
+ aHandleReport->Callback(NS_LITERAL_CSTRING("System"), _path, \
+ KIND_OTHER, _units, __amount, _desc, aData); \
+ } \
+ } while (0)
+
+ NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) override
+ {
+ // There is lots of privacy-sensitive data in /proc. Just skip this
+ // reporter entirely when anonymization is required.
+ if (aAnonymize) {
+ return NS_OK;
+ }
+
+ if (!Preferences::GetBool("memory.system_memory_reporter")) {
+ return NS_OK;
+ }
+
+ // Read relevant fields from /proc/meminfo.
+ int64_t memTotal = 0, memFree = 0;
+ nsresult rv1 = ReadMemInfo(&memTotal, &memFree);
+
+ // Collect per-process reports from /proc/<pid>/smaps.
+ int64_t totalPss = 0;
+ nsresult rv2 = CollectProcessReports(aHandleReport, aData, &totalPss);
+
+ // Report the non-process numbers.
+ if (NS_SUCCEEDED(rv1) && NS_SUCCEEDED(rv2)) {
+ int64_t other = memTotal - memFree - totalPss;
+ REPORT(NS_LITERAL_CSTRING("mem/other"), UNITS_BYTES, other,
+ NS_LITERAL_CSTRING(
+"Memory which is neither owned by any user-space process nor free. Note that "
+"this includes memory holding cached files from the disk which can be "
+"reclaimed by the OS at any time."));
+
+ REPORT(NS_LITERAL_CSTRING("mem/free"), UNITS_BYTES, memFree,
+ NS_LITERAL_CSTRING(
+"Memory which is free and not being used for any purpose."));
+ }
+
+ // Report reserved memory not included in memTotal.
+ CollectPmemReports(aHandleReport, aData);
+
+ // Report zram usage statistics.
+ CollectZramReports(aHandleReport, aData);
+
+ // Report kgsl graphics memory usage.
+ CollectKgslReports(aHandleReport, aData);
+
+ // Report ION memory usage.
+ CollectIonReports(aHandleReport, aData);
+
+ return NS_OK;
+ }
+
+private:
+ // These are the cross-cutting measurements across all processes.
+ class ProcessSizes
+ {
+ public:
+ void Add(const nsACString& aKey, size_t aSize)
+ {
+ mTagged.Put(aKey, mTagged.Get(aKey) + aSize);
+ }
+
+ void Report(nsIHandleReportCallback* aHandleReport, nsISupports* aData)
+ {
+ for (auto iter = mTagged.Iter(); !iter.Done(); iter.Next()) {
+ nsCStringHashKey::KeyType key = iter.Key();
+ size_t amount = iter.UserData();
+
+ nsAutoCString path("processes/");
+ path.Append(key);
+
+ nsAutoCString desc("This is the sum of all processes' '");
+ desc.Append(key);
+ desc.AppendLiteral("' numbers.");
+
+ REPORT(path, UNITS_BYTES, amount, desc);
+ }
+ }
+
+ private:
+ nsDataHashtable<nsCStringHashKey, size_t> mTagged;
+ };
+
+ nsresult ReadMemInfo(int64_t* aMemTotal, int64_t* aMemFree)
+ {
+ FILE* f = fopen("/proc/meminfo", "r");
+ if (!f) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int n1 = fscanf(f, "MemTotal: %" SCNd64 " kB\n", aMemTotal);
+ int n2 = fscanf(f, "MemFree: %" SCNd64 " kB\n", aMemFree);
+
+ fclose(f);
+
+ if (n1 != 1 || n2 != 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Convert from KB to B.
+ *aMemTotal *= 1024;
+ *aMemFree *= 1024;
+
+ return NS_OK;
+ }
+
+ nsresult CollectProcessReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData,
+ int64_t* aTotalPss)
+ {
+ *aTotalPss = 0;
+ ProcessSizes processSizes;
+
+ DIR* d = opendir("/proc");
+ if (NS_WARN_IF(!d)) {
+ return NS_ERROR_FAILURE;
+ }
+ struct dirent* ent;
+ while ((ent = readdir(d))) {
+ struct stat statbuf;
+ const char* pidStr = ent->d_name;
+ // Don't check the return value of stat() -- it can return -1 for these
+ // directories even when it has succeeded, apparently.
+ stat(pidStr, &statbuf);
+ if (S_ISDIR(statbuf.st_mode) && IsNumeric(pidStr)) {
+ nsCString processName("process(");
+
+ // Get the command name from cmdline. If that fails, the pid is still
+ // shown.
+ nsPrintfCString cmdlinePath("/proc/%s/cmdline", pidStr);
+ FILE* f = fopen(cmdlinePath.get(), "r");
+ if (f) {
+ static const size_t len = 256;
+ char buf[len];
+ if (fgets(buf, len, f)) {
+ processName.Append(buf);
+ // A hack: replace forward slashes with '\\' so they aren't treated
+ // as path separators. Consumers of this reporter (such as
+ // about:memory) have to undo this change.
+ processName.ReplaceChar('/', '\\');
+ processName.AppendLiteral(", ");
+ }
+ fclose(f);
+ }
+ processName.AppendLiteral("pid=");
+ processName.Append(pidStr);
+ processName.Append(')');
+
+ // Read the PSS values from the smaps file.
+ nsPrintfCString smapsPath("/proc/%s/smaps", pidStr);
+ f = fopen(smapsPath.get(), "r");
+ if (!f) {
+ // Processes can terminate between the readdir() call above and now,
+ // so just skip if we can't open the file.
+ continue;
+ }
+ ParseMappings(f, processName, aHandleReport, aData, &processSizes,
+ aTotalPss);
+ fclose(f);
+
+ // Report the open file descriptors for this process.
+ nsPrintfCString procFdPath("/proc/%s/fd", pidStr);
+ CollectOpenFileReports(aHandleReport, aData, procFdPath, processName);
+ }
+ }
+ closedir(d);
+
+ // Report the "processes/" tree.
+ processSizes.Report(aHandleReport, aData);
+
+ return NS_OK;
+ }
+
+ void ParseMappings(FILE* aFile,
+ const nsACString& aProcessName,
+ nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData,
+ ProcessSizes* aProcessSizes,
+ int64_t* aTotalPss)
+ {
+ // The first line of an entry in /proc/<pid>/smaps looks just like an entry
+ // in /proc/<pid>/maps:
+ //
+ // address perms offset dev inode pathname
+ // 02366000-025d8000 rw-p 00000000 00:00 0 [heap]
+ //
+ // Each of the following lines contains a key and a value, separated
+ // by ": ", where the key does not contain either of those characters.
+ // Assuming more than this about the structure of those lines has
+ // failed to be future-proof in the past, so we avoid doing so.
+ //
+ // This makes it difficult to detect the start of a new entry
+ // until it's been removed from the stdio buffer, so we just loop
+ // over all lines in the file in this routine.
+
+ const int argCount = 8;
+
+ unsigned long long addrStart, addrEnd;
+ char perms[5];
+ unsigned long long offset;
+ // The 2.6 and 3.0 kernels allocate 12 bits for the major device number and
+ // 20 bits for the minor device number. Future kernels might allocate more.
+ // 64 bits ought to be enough for anybody.
+ char devMajor[17];
+ char devMinor[17];
+ unsigned int inode;
+ char line[1025];
+
+ // This variable holds the path of the current entry, or is void
+ // if we're scanning for the start of a new entry.
+ nsAutoCString currentPath;
+ int pathOffset;
+
+ currentPath.SetIsVoid(true);
+ while (fgets(line, sizeof(line), aFile)) {
+ if (currentPath.IsVoid()) {
+ int n = sscanf(line,
+ "%llx-%llx %4s %llx "
+ "%16[0-9a-fA-F]:%16[0-9a-fA-F] %u %n",
+ &addrStart, &addrEnd, perms, &offset, devMajor,
+ devMinor, &inode, &pathOffset);
+
+ if (n >= argCount - 1) {
+ currentPath.Assign(line + pathOffset);
+ currentPath.StripChars("\n");
+ }
+ continue;
+ }
+
+ // Now that we have a name and other metadata, scan for the PSS.
+ size_t pss_kb;
+ int n = sscanf(line, "Pss: %zu", &pss_kb);
+ if (n < 1) {
+ continue;
+ }
+
+ size_t pss = pss_kb * 1024;
+ if (pss > 0) {
+ nsAutoCString name, description, tag;
+ GetReporterNameAndDescription(currentPath.get(), perms, name, description, tag);
+
+ nsAutoCString processMemPath("mem/processes/");
+ processMemPath.Append(aProcessName);
+ processMemPath.Append('/');
+ processMemPath.Append(name);
+
+ REPORT(processMemPath, UNITS_BYTES, pss, description);
+
+ // Increment the appropriate aProcessSizes values, and the total.
+ aProcessSizes->Add(tag, pss);
+ *aTotalPss += pss;
+ }
+
+ // Now that we've seen the PSS, we're done with this entry.
+ currentPath.SetIsVoid(true);
+ }
+ }
+
+ void GetReporterNameAndDescription(const char* aPath,
+ const char* aPerms,
+ nsACString& aName,
+ nsACString& aDesc,
+ nsACString& aTag)
+ {
+ aName.Truncate();
+ aDesc.Truncate();
+ aTag.Truncate();
+
+ // If aPath points to a file, we have its absolute path; it might
+ // also be a bracketed pseudo-name (see below). In either case
+ // there is also some whitespace to trim.
+ nsAutoCString absPath;
+ absPath.Append(aPath);
+ absPath.StripChars(" ");
+
+ if (absPath.EqualsLiteral("[heap]")) {
+ aName.AppendLiteral("anonymous/brk-heap");
+ aDesc.AppendLiteral(
+ "Memory in anonymous mappings within the boundaries defined by "
+ "brk() / sbrk(). This is likely to be just a portion of the "
+ "application's heap; the remainder lives in other anonymous mappings. "
+ "This corresponds to '[heap]' in /proc/<pid>/smaps.");
+ aTag = aName;
+ } else if (absPath.EqualsLiteral("[stack]")) {
+ aName.AppendLiteral("stack/main-thread");
+ aDesc.AppendPrintf(
+ "The stack size of the process's main thread. This corresponds to "
+ "'[stack]' in /proc/<pid>/smaps.");
+ aTag = aName;
+ } else if (MozTaggedMemoryIsSupported() &&
+ StringBeginsWith(absPath, NS_LITERAL_CSTRING("[stack:"))) {
+ // If tagged memory is supported, we can be reasonably sure that
+ // the virtual memory area containing the stack hasn't been
+ // merged with unrelated heap memory. (This prevents the
+ // "[stack:" entries from reaching the IsAnonymous case below.)
+ pid_t tid = atoi(absPath.get() + 7);
+ nsAutoCString threadName, escapedThreadName;
+ LinuxUtils::GetThreadName(tid, threadName);
+ if (threadName.IsEmpty()) {
+ threadName.AssignLiteral("<unknown>");
+ }
+ escapedThreadName.Assign(threadName);
+ escapedThreadName.StripChars("()");
+ escapedThreadName.ReplaceChar('/', '\\');
+
+ aName.AppendLiteral("stack/non-main-thread");
+ aName.AppendLiteral("/name(");
+ aName.Append(escapedThreadName);
+ aName.Append(')');
+ aTag = aName;
+ aName.AppendPrintf("/thread(%d)", tid);
+
+ aDesc.AppendPrintf("The stack size of a non-main thread named '%s' with "
+ "thread ID %d. This corresponds to '[stack:%d]' "
+ "in /proc/%d/smaps.", threadName.get(), tid, tid);
+ } else if (absPath.EqualsLiteral("[vdso]")) {
+ aName.AppendLiteral("vdso");
+ aDesc.AppendLiteral(
+ "The virtual dynamically-linked shared object, also known as the "
+ "'vsyscall page'. This is a memory region mapped by the operating "
+ "system for the purpose of allowing processes to perform some "
+ "privileged actions without the overhead of a syscall.");
+ aTag = aName;
+ } else if (StringBeginsWith(absPath, NS_LITERAL_CSTRING("[anon:")) &&
+ EndsWithLiteral(absPath, "]")) {
+ // It's tagged memory; see also "mfbt/TaggedAnonymousMemory.h".
+ nsAutoCString tag(Substring(absPath, 6, absPath.Length() - 7));
+
+ aName.AppendLiteral("anonymous/");
+ aName.Append(tag);
+ aTag = aName;
+ aDesc.AppendLiteral("Memory in anonymous mappings tagged with '");
+ aDesc.Append(tag);
+ aDesc.Append('\'');
+ } else if (!IsAnonymous(absPath)) {
+ // We now know it's an actual file. Truncate this to its
+ // basename, and put the absolute path in the description.
+ nsAutoCString basename, dirname;
+ GetBasename(absPath, basename);
+ GetDirname(absPath, dirname);
+
+ // Hack: A file is a shared library if the basename contains ".so" and
+ // its dirname contains "/lib", or if the basename ends with ".so".
+ if (EndsWithLiteral(basename, ".so") ||
+ (basename.Find(".so") != -1 && dirname.Find("/lib") != -1)) {
+ aName.AppendLiteral("shared-libraries/");
+ aTag = aName;
+
+ if (strncmp(aPerms, "r-x", 3) == 0) {
+ aTag.AppendLiteral("read-executable");
+ } else if (strncmp(aPerms, "rw-", 3) == 0) {
+ aTag.AppendLiteral("read-write");
+ } else if (strncmp(aPerms, "r--", 3) == 0) {
+ aTag.AppendLiteral("read-only");
+ } else {
+ aTag.AppendLiteral("other");
+ }
+
+ } else {
+ aName.AppendLiteral("other-files");
+ if (EndsWithLiteral(basename, ".xpi")) {
+ aName.AppendLiteral("/extensions");
+ } else if (dirname.Find("/fontconfig") != -1) {
+ aName.AppendLiteral("/fontconfig");
+ } else {
+ aName.AppendLiteral("/misc");
+ }
+ aTag = aName;
+ aName.Append('/');
+ }
+
+ aName.Append(basename);
+ aDesc.Append(absPath);
+ } else {
+ if (MozTaggedMemoryIsSupported()) {
+ aName.AppendLiteral("anonymous/untagged");
+ aDesc.AppendLiteral("Memory in untagged anonymous mappings.");
+ aTag = aName;
+ } else {
+ aName.AppendLiteral("anonymous/outside-brk");
+ aDesc.AppendLiteral("Memory in anonymous mappings outside the "
+ "boundaries defined by brk() / sbrk().");
+ aTag = aName;
+ }
+ }
+
+ aName.AppendLiteral("/[");
+ aName.Append(aPerms);
+ aName.Append(']');
+
+ // Append the permissions. This is useful for non-verbose mode in
+ // about:memory when the filename is long and goes of the right side of the
+ // window.
+ aDesc.AppendLiteral(" [");
+ aDesc.Append(aPerms);
+ aDesc.Append(']');
+ }
+
+ void CollectPmemReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData)
+ {
+ // The pmem subsystem allocates physically contiguous memory for
+ // interfacing with hardware. In order to ensure availability,
+ // this memory is reserved during boot, and allocations are made
+ // within these regions at runtime.
+ //
+ // There are typically several of these pools allocated at boot.
+ // The /sys/kernel/pmem_regions directory contains a subdirectory
+ // for each one. Within each subdirectory, the files we care
+ // about are "size" (the total amount of physical memory) and
+ // "mapped_regions" (a list of the current allocations within that
+ // area).
+ DIR* d = opendir("/sys/kernel/pmem_regions");
+ if (!d) {
+ return;
+ }
+
+ struct dirent* ent;
+ while ((ent = readdir(d))) {
+ const char* name = ent->d_name;
+ uint64_t size;
+ int scanned;
+
+ // Skip "." and ".." (and any other dotfiles).
+ if (name[0] == '.') {
+ continue;
+ }
+
+ // Read the total size. The file gives the size in decimal and
+ // hex, in the form "13631488(0xd00000)"; we parse the former.
+ nsPrintfCString sizePath("/sys/kernel/pmem_regions/%s/size", name);
+ FILE* sizeFile = fopen(sizePath.get(), "r");
+ if (NS_WARN_IF(!sizeFile)) {
+ continue;
+ }
+ scanned = fscanf(sizeFile, "%" SCNu64, &size);
+ fclose(sizeFile);
+ if (NS_WARN_IF(scanned != 1)) {
+ continue;
+ }
+
+ // Read mapped regions; format described below.
+ uint64_t freeSize = size;
+ nsPrintfCString regionsPath("/sys/kernel/pmem_regions/%s/mapped_regions",
+ name);
+ FILE* regionsFile = fopen(regionsPath.get(), "r");
+ if (regionsFile) {
+ static const size_t bufLen = 4096;
+ char buf[bufLen];
+ while (fgets(buf, bufLen, regionsFile)) {
+ int pid;
+
+ // Skip header line.
+ if (strncmp(buf, "pid #", 5) == 0) {
+ continue;
+ }
+ // Line format: "pid N:" + zero or more "(Start,Len) ".
+ // N is decimal; Start and Len are in hex.
+ scanned = sscanf(buf, "pid %d", &pid);
+ if (NS_WARN_IF(scanned != 1)) {
+ continue;
+ }
+ for (const char* nextParen = strchr(buf, '(');
+ nextParen != nullptr;
+ nextParen = strchr(nextParen + 1, '(')) {
+ uint64_t mapStart, mapLen;
+
+ scanned = sscanf(nextParen + 1, "%" SCNx64 ",%" SCNx64,
+ &mapStart, &mapLen);
+ if (NS_WARN_IF(scanned != 2)) {
+ break;
+ }
+
+ nsPrintfCString path("mem/pmem/used/%s/segment(pid=%d, "
+ "offset=0x%" PRIx64 ")", name, pid, mapStart);
+ nsPrintfCString desc("Physical memory reserved for the \"%s\" pool "
+ "and allocated to a buffer.", name);
+ REPORT(path, UNITS_BYTES, mapLen, desc);
+ freeSize -= mapLen;
+ }
+ }
+ fclose(regionsFile);
+ }
+
+ nsPrintfCString path("mem/pmem/free/%s", name);
+ nsPrintfCString desc("Physical memory reserved for the \"%s\" pool and "
+ "unavailable to the rest of the system, but not "
+ "currently allocated.", name);
+ REPORT(path, UNITS_BYTES, freeSize, desc);
+ }
+ closedir(d);
+ }
+
+ void
+ CollectIonReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData)
+ {
+ // ION is a replacement for PMEM (and other similar allocators).
+ //
+ // More details from http://lwn.net/Articles/480055/
+ // "Like its PMEM-like predecessors, ION manages one or more memory pools,
+ // some of which are set aside at boot time to combat fragmentation or to
+ // serve special hardware needs. GPUs, display controllers, and cameras
+ // are some of the hardware blocks that may have special memory
+ // requirements."
+ //
+ // The file format starts as follows:
+ // client pid size
+ // ----------------------------------------------------
+ // adsprpc-smd 1 4096
+ // fd900000.qcom,mdss_mdp 1 1658880
+ // ----------------------------------------------------
+ // orphaned allocations (info is from last known client):
+ // Homescreen 24100 294912 0 1
+ // b2g 23987 1658880 0 1
+ // mdss_fb0 401 1658880 0 1
+ // b2g 23987 4096 0 1
+ // Built-in Keyboa 24205 61440 0 1
+ // ----------------------------------------------------
+ // <other stuff>
+ //
+ // For our purposes we only care about the first portion of the file noted
+ // above which contains memory alloations (both sections). The term
+ // "orphaned" is misleading, it appears that every allocation not by the
+ // first process is considered orphaned on FxOS devices.
+
+ // The first three fields of each entry interest us:
+ // 1) client - Essentially the process name. We limit client names to 63
+ // characters, in theory they should never be greater than 15
+ // due to thread name length limitations.
+ // 2) pid - The ID of the allocating process, read as a uint32_t.
+ // 3) size - The size of the allocation in bytes, read as as a uint64_t.
+ const char* const kFormatString = "%63s %" SCNu32 " %" SCNu64;
+ const int kNumFields = 3;
+ const size_t kStringSize = 64;
+ const char* const kIonIommuPath = "/sys/kernel/debug/ion/iommu";
+
+ FILE* iommu = fopen(kIonIommuPath, "r");
+ if (!iommu) {
+ return;
+ }
+
+ AutoFile iommuGuard(iommu);
+
+ const size_t kBufferLen = 256;
+ char buffer[kBufferLen];
+ char client[kStringSize];
+ uint32_t pid;
+ uint64_t size;
+
+ // Ignore the header line.
+ Unused << fgets(buffer, kBufferLen, iommu);
+
+ // Ignore the separator line.
+ Unused << fgets(buffer, kBufferLen, iommu);
+
+ const char* const kSep = "----";
+ const size_t kSepLen = 4;
+
+ // Read non-orphaned entries.
+ while (fgets(buffer, kBufferLen, iommu) &&
+ strncmp(kSep, buffer, kSepLen) != 0) {
+ if (sscanf(buffer, kFormatString, client, &pid, &size) == kNumFields) {
+ nsPrintfCString entryPath("ion-memory/%s (pid=%d)", client, pid);
+ REPORT(entryPath, UNITS_BYTES, size,
+ NS_LITERAL_CSTRING("An ION kernel memory allocation."));
+ }
+ }
+
+ // Ignore the orphaned header.
+ Unused << fgets(buffer, kBufferLen, iommu);
+
+ // Read orphaned entries.
+ while (fgets(buffer, kBufferLen, iommu) &&
+ strncmp(kSep, buffer, kSepLen) != 0) {
+ if (sscanf(buffer, kFormatString, client, &pid, &size) == kNumFields) {
+ nsPrintfCString entryPath("ion-memory/%s (pid=%d)", client, pid);
+ REPORT(entryPath, UNITS_BYTES, size,
+ NS_LITERAL_CSTRING("An ION kernel memory allocation."));
+ }
+ }
+
+ // Ignore the rest of the file.
+ }
+
+ uint64_t
+ ReadSizeFromFile(const char* aFilename)
+ {
+ FILE* sizeFile = fopen(aFilename, "r");
+ if (NS_WARN_IF(!sizeFile)) {
+ return 0;
+ }
+
+ uint64_t size = 0;
+ Unused << fscanf(sizeFile, "%" SCNu64, &size);
+ fclose(sizeFile);
+
+ return size;
+ }
+
+ void
+ CollectZramReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData)
+ {
+ // zram usage stats files can be found under:
+ // /sys/block/zram<id>
+ // |--> disksize - Maximum amount of uncompressed data that can be
+ // stored on the disk (bytes)
+ // |--> orig_data_size - Uncompressed size of data in the disk (bytes)
+ // |--> compr_data_size - Compressed size of the data in the disk (bytes)
+ // |--> num_reads - Number of attempted reads to the disk (count)
+ // |--> num_writes - Number of attempted writes to the disk (count)
+ //
+ // Each file contains a single integer value in decimal form.
+
+ DIR* d = opendir("/sys/block");
+ if (!d) {
+ return;
+ }
+
+ struct dirent* ent;
+ while ((ent = readdir(d))) {
+ const char* name = ent->d_name;
+
+ // Skip non-zram entries.
+ if (strncmp("zram", name, 4) != 0) {
+ continue;
+ }
+
+ // Report disk size statistics.
+ nsPrintfCString diskSizeFile("/sys/block/%s/disksize", name);
+ nsPrintfCString origSizeFile("/sys/block/%s/orig_data_size", name);
+
+ uint64_t diskSize = ReadSizeFromFile(diskSizeFile.get());
+ uint64_t origSize = ReadSizeFromFile(origSizeFile.get());
+ uint64_t unusedSize = diskSize - origSize;
+
+ nsPrintfCString diskUsedPath("zram-disksize/%s/used", name);
+ nsPrintfCString diskUsedDesc(
+ "The uncompressed size of data stored in \"%s.\" "
+ "This excludes zero-filled pages since "
+ "no memory is allocated for them.", name);
+ REPORT(diskUsedPath, UNITS_BYTES, origSize, diskUsedDesc);
+
+ nsPrintfCString diskUnusedPath("zram-disksize/%s/unused", name);
+ nsPrintfCString diskUnusedDesc(
+ "The amount of uncompressed data that can still be "
+ "be stored in \"%s\"", name);
+ REPORT(diskUnusedPath, UNITS_BYTES, unusedSize, diskUnusedDesc);
+
+ // Report disk accesses.
+ nsPrintfCString readsFile("/sys/block/%s/num_reads", name);
+ nsPrintfCString writesFile("/sys/block/%s/num_writes", name);
+
+ uint64_t reads = ReadSizeFromFile(readsFile.get());
+ uint64_t writes = ReadSizeFromFile(writesFile.get());
+
+ nsPrintfCString readsDesc(
+ "The number of reads (failed or successful) done on "
+ "\"%s\"", name);
+ nsPrintfCString readsPath("zram-accesses/%s/reads", name);
+ REPORT(readsPath, UNITS_COUNT_CUMULATIVE, reads, readsDesc);
+
+ nsPrintfCString writesDesc(
+ "The number of writes (failed or successful) done "
+ "on \"%s\"", name);
+ nsPrintfCString writesPath("zram-accesses/%s/writes", name);
+ REPORT(writesPath, UNITS_COUNT_CUMULATIVE, writes, writesDesc);
+
+ // Report compressed data size.
+ nsPrintfCString comprSizeFile("/sys/block/%s/compr_data_size", name);
+ uint64_t comprSize = ReadSizeFromFile(comprSizeFile.get());
+
+ nsPrintfCString comprSizeDesc(
+ "The compressed size of data stored in \"%s\"",
+ name);
+ nsPrintfCString comprSizePath("zram-compr-data-size/%s", name);
+ REPORT(comprSizePath, UNITS_BYTES, comprSize, comprSizeDesc);
+ }
+
+ closedir(d);
+ }
+
+ void
+ CollectOpenFileReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData,
+ const nsACString& aProcPath,
+ const nsACString& aProcessName)
+ {
+ // All file descriptors opened by a process are listed under
+ // /proc/<pid>/fd/<numerical_fd>. Each entry is a symlink that points to the
+ // path that was opened. This can be an actual file, a socket, a pipe, an
+ // anon_inode, or possibly an uncategorized device.
+ const char kFilePrefix[] = "/";
+ const char kSocketPrefix[] = "socket:";
+ const char kPipePrefix[] = "pipe:";
+ const char kAnonInodePrefix[] = "anon_inode:";
+
+ const nsCString procPath(aProcPath);
+ DIR* d = opendir(procPath.get());
+ if (!d) {
+ return;
+ }
+
+ char linkPath[PATH_MAX + 1];
+ struct dirent* ent;
+ while ((ent = readdir(d))) {
+ const char* fd = ent->d_name;
+
+ // Skip "." and ".." (and any other dotfiles).
+ if (fd[0] == '.') {
+ continue;
+ }
+
+ nsPrintfCString fullPath("%s/%s", procPath.get(), fd);
+ ssize_t linkPathSize = readlink(fullPath.get(), linkPath, PATH_MAX);
+ if (linkPathSize > 0) {
+ linkPath[linkPathSize] = '\0';
+
+#define CHECK_PREFIX(prefix) \
+ (strncmp(linkPath, prefix, sizeof(prefix) - 1) == 0)
+
+ const char* category = nullptr;
+ const char* descriptionPrefix = nullptr;
+
+ if (CHECK_PREFIX(kFilePrefix)) {
+ category = "files"; // No trailing slash, the file path will have one
+ descriptionPrefix = "An open";
+ } else if (CHECK_PREFIX(kSocketPrefix)) {
+ category = "sockets/";
+ descriptionPrefix = "A socket";
+ } else if (CHECK_PREFIX(kPipePrefix)) {
+ category = "pipes/";
+ descriptionPrefix = "A pipe";
+ } else if (CHECK_PREFIX(kAnonInodePrefix)) {
+ category = "anon_inodes/";
+ descriptionPrefix = "An anon_inode";
+ } else {
+ category = "";
+ descriptionPrefix = "An uncategorized";
+ }
+
+#undef CHECK_PREFIX
+
+ const nsCString processName(aProcessName);
+ nsPrintfCString entryPath("open-fds/%s/%s%s/%s",
+ processName.get(), category, linkPath, fd);
+ nsPrintfCString entryDescription("%s file descriptor opened by the process",
+ descriptionPrefix);
+ REPORT(entryPath, UNITS_COUNT, 1, entryDescription);
+ }
+ }
+
+ closedir(d);
+ }
+
+ void
+ CollectKgslReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData)
+ {
+ // Each process that uses kgsl memory will have an entry under
+ // /sys/kernel/debug/kgsl/proc/<pid>/mem. This file format includes a
+ // header and then entries with types as follows:
+ // gpuaddr useraddr size id flags type usage sglen
+ // hexaddr hexaddr int int str str str int
+ // We care primarily about the usage and size.
+
+ // For simplicity numbers will be uint64_t, strings 63 chars max.
+ const char* const kScanFormat =
+ "%" SCNx64 " %" SCNx64 " %" SCNu64 " %" SCNu64
+ " %63s %63s %63s %" SCNu64;
+ const int kNumFields = 8;
+ const size_t kStringSize = 64;
+
+ DIR* d = opendir("/sys/kernel/debug/kgsl/proc/");
+ if (!d) {
+ return;
+ }
+
+ AutoDir dirGuard(d);
+
+ struct dirent* ent;
+ while ((ent = readdir(d))) {
+ const char* pid = ent->d_name;
+
+ // Skip "." and ".." (and any other dotfiles).
+ if (pid[0] == '.') {
+ continue;
+ }
+
+ nsPrintfCString memPath("/sys/kernel/debug/kgsl/proc/%s/mem", pid);
+ FILE* memFile = fopen(memPath.get(), "r");
+ if (NS_WARN_IF(!memFile)) {
+ continue;
+ }
+
+ AutoFile fileGuard(memFile);
+
+ // Attempt to map the pid to a more useful name.
+ nsAutoCString procName;
+ LinuxUtils::GetThreadName(atoi(pid), procName);
+
+ if (procName.IsEmpty()) {
+ procName.Append("pid=");
+ procName.Append(pid);
+ } else {
+ procName.Append(" (pid=");
+ procName.Append(pid);
+ procName.Append(")");
+ }
+
+ uint64_t gpuaddr, useraddr, size, id, sglen;
+ char flags[kStringSize];
+ char type[kStringSize];
+ char usage[kStringSize];
+
+ // Bypass the header line.
+ char buff[1024];
+ Unused << fgets(buff, 1024, memFile);
+
+ while (fscanf(memFile, kScanFormat, &gpuaddr, &useraddr, &size, &id,
+ flags, type, usage, &sglen) == kNumFields) {
+ nsPrintfCString entryPath("kgsl-memory/%s/%s", procName.get(), usage);
+ REPORT(entryPath, UNITS_BYTES, size,
+ NS_LITERAL_CSTRING("A kgsl graphics memory allocation."));
+ }
+ }
+ }
+};
+
+NS_IMPL_ISUPPORTS(SystemReporter, nsIMemoryReporter)
+
+void
+Init()
+{
+ RegisterStrongMemoryReporter(new SystemReporter());
+}
+
+} // namespace SystemMemoryReporter
+} // namespace mozilla