/* -*- 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/. */ #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 #include #include #include #include #include #include // 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//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 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//smaps looks just like an entry // in /proc//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//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//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(""); } 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 // ---------------------------------------------------- // // // 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 // |--> 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//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//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