diff options
Diffstat (limited to 'xpcom/base/SystemMemoryReporter.cpp')
-rw-r--r-- | xpcom/base/SystemMemoryReporter.cpp | 989 |
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 |