/* -*- Mode: C++; tab-width: 50; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 ci et: */ /* 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/Util.h" #include "mozilla/MapsMemoryReporter.h" #include "nsIMemoryReporter.h" #include "nsString.h" #include "nsCOMPtr.h" #include "nsTHashtable.h" #include "nsHashKeys.h" #include "mozilla/Attributes.h" #include "mozilla/unused.h" #include namespace mozilla { namespace MapsMemoryReporter { #if !defined(XP_LINUX) #error "This doesn't have a prayer of working if we're not on Linux." #endif // mozillaLibraries is a list of all the shared libraries we build. This list // is used for determining whether a library is a "Mozilla library" or a // "third-party library". But even if this list is missing items, about:memory // will identify a library in the same directory as libxul.so as a "Mozilla // library". const char* mozillaLibraries[] = { "libfreebl3.so", "libmozalloc.so", "libmozsqlite3.so", "libnspr4.so", "libnss3.so", "libnssckbi.so", "libnssdbm3.so", "libnssutil3.so", "libplc4.so", "libplds4.so", "libsmime3.so", "libsoftokn3.so", "libssl3.so", "libxpcom.so", "libxul.so" }; namespace { bool EndsWithLiteral(const nsCString &aHaystack, const char *aNeedle) { int32_t idx = aHaystack.RFind(aNeedle); if (idx == -1) { return false; } return idx + strlen(aNeedle) == aHaystack.Length(); } void GetDirname(const nsCString &aPath, nsACString &aOut) { int32_t idx = aPath.RFind("/"); if (idx == -1) { aOut.Truncate(); } else { aOut.Assign(Substring(aPath, 0, idx)); } } 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); } // MapsReporter::CollectReports uses this stuct to keep track of whether it's // seen a mapping under 'rss', 'pss', 'size', and 'swap'. struct CategoriesSeen { CategoriesSeen() : mSeenRss(false), mSeenPss(false), mSeenSize(false), mSeenSwap(false) { } bool mSeenRss; bool mSeenPss; bool mSeenSize; bool mSeenSwap; }; } // anonymous namespace class MapsReporter MOZ_FINAL : public nsIMemoryMultiReporter { public: MapsReporter(); NS_DECL_ISUPPORTS NS_IMETHOD GetName(nsACString &aName) { aName.AssignLiteral("smaps"); return NS_OK; } NS_IMETHOD CollectReports(nsIMemoryMultiReporterCallback *aCb, nsISupports *aClosure); NS_IMETHOD GetExplicitNonHeap(int64_t *aAmount) { // This reporter doesn't do any "explicit" measurements. *aAmount = 0; return NS_OK; } private: // Search through /proc/self/maps for libxul.so, and set mLibxulDir to the // the directory containing libxul. nsresult FindLibxul(); nsresult ParseMapping(FILE *aFile, nsIMemoryMultiReporterCallback *aCb, nsISupports *aClosure, CategoriesSeen *aCategoriesSeen); void GetReporterNameAndDescription(const char *aPath, const char *aPermissions, nsACString &aName, nsACString &aDesc); nsresult ParseMapBody(FILE *aFile, const nsACString &aName, const nsACString &aDescription, nsIMemoryMultiReporterCallback *aCb, nsISupports *aClosure, CategoriesSeen *aCategoriesSeen); bool mSearchedForLibxul; nsCString mLibxulDir; nsTHashtable mMozillaLibraries; }; NS_IMPL_THREADSAFE_ISUPPORTS1(MapsReporter, nsIMemoryMultiReporter) MapsReporter::MapsReporter() : mSearchedForLibxul(false) { const uint32_t len = ArrayLength(mozillaLibraries); mMozillaLibraries.Init(len); for (uint32_t i = 0; i < len; i++) { nsAutoCString str; str.Assign(mozillaLibraries[i]); mMozillaLibraries.PutEntry(str); } } NS_IMETHODIMP MapsReporter::CollectReports(nsIMemoryMultiReporterCallback *aCb, nsISupports *aClosure) { CategoriesSeen categoriesSeen; FILE *f = fopen("/proc/self/smaps", "r"); if (!f) return NS_ERROR_FAILURE; while (true) { nsresult rv = ParseMapping(f, aCb, aClosure, &categoriesSeen); if (NS_FAILED(rv)) break; } fclose(f); // For sure we should have created some node under 'rss' and // 'size'; otherwise we're probably not reading smaps correctly. If we // didn't create a node under 'swap', create one here so about:memory // knows to create an empty 'swap' tree; it needs a 'total' child because // about:memory expects at least one report whose path begins with 'swap/'. NS_ASSERTION(categoriesSeen.mSeenSize, "Didn't create a size node?"); NS_ASSERTION(categoriesSeen.mSeenRss, "Didn't create a rss node?"); NS_ASSERTION(categoriesSeen.mSeenPss, "Didn't create a pss node?"); if (!categoriesSeen.mSeenSwap) { nsresult rv; rv = aCb->Callback(NS_LITERAL_CSTRING(""), NS_LITERAL_CSTRING("swap/total"), nsIMemoryReporter::KIND_NONHEAP, nsIMemoryReporter::UNITS_BYTES, 0, NS_LITERAL_CSTRING("This process uses no swap space."), aClosure); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult MapsReporter::FindLibxul() { if (mSearchedForLibxul) return NS_OK; mSearchedForLibxul = true; mLibxulDir.Truncate(); // Note that we're scanning /proc/self/*maps*, not smaps, here. FILE *f = fopen("/proc/self/maps", "r"); if (!f) { return NS_ERROR_FAILURE; } while (true) { // Skip any number of non-slash characters, then capture starting with the // slash to the newline. This is the path part of /proc/self/maps. char path[1025]; int numRead = fscanf(f, "%*[^/]%1024[^\n]", path); if (numRead != 1) { break; } nsAutoCString pathStr; pathStr.Append(path); nsAutoCString basename; GetBasename(pathStr, basename); if (basename.EqualsLiteral("libxul.so")) { GetDirname(pathStr, mLibxulDir); break; } } fclose(f); return mLibxulDir.IsEmpty() ? NS_ERROR_FAILURE : NS_OK; } nsresult MapsReporter::ParseMapping( FILE *aFile, nsIMemoryMultiReporterCallback *aCb, nsISupports *aClosure, CategoriesSeen *aCategoriesSeen) { // We need to use native types in order to get good warnings from fscanf, so // let's make sure that the native types have the sizes we expect. MOZ_STATIC_ASSERT(sizeof(long long) == sizeof(int64_t), "size of (long long) is expected to match (int64_t)"); MOZ_STATIC_ASSERT(sizeof(int) == sizeof(int32_t), "size of (int) is expected to match (int32_t)"); // Don't bail if FindLibxul fails. We can still gather meaningful stats // here. FindLibxul(); // The first line of an entry in /proc/self/smaps looks just like an entry // in /proc/maps: // // address perms offset dev inode pathname // 02366000-025d8000 rw-p 00000000 00:00 0 [heap] 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 path[1025]; // A path might not be present on this line; set it to the empty string. path[0] = '\0'; // This is a bit tricky. Whitespace in a scanf pattern matches *any* // whitespace, including newlines. We want this pattern to match a line // with or without a path, but we don't want to look to a new line for the // path. Thus we have %u%1024[^\n] at the end of the pattern. This will // capture into the path some leading whitespace, which we'll later trim off. int numRead = fscanf(aFile, "%llx-%llx %4s %llx " "%16[0-9a-fA-F]:%16[0-9a-fA-F] %u%1024[^\n]", &addrStart, &addrEnd, perms, &offset, devMajor, devMinor, &inode, path); // Eat up any whitespace at the end of this line, including the newline. unused << fscanf(aFile, " "); // We might or might not have a path, but the rest of the arguments should be // there. if (numRead != argCount && numRead != argCount - 1) { return NS_ERROR_FAILURE; } nsAutoCString name, description; GetReporterNameAndDescription(path, perms, name, description); while (true) { nsresult rv = ParseMapBody(aFile, name, description, aCb, aClosure, aCategoriesSeen); if (NS_FAILED(rv)) break; } return NS_OK; } void MapsReporter::GetReporterNameAndDescription( const char *aPath, const char *aPerms, nsACString &aName, nsACString &aDesc) { aName.Truncate(); aDesc.Truncate(); // If aPath points to a file, we have its absolute path, plus some // whitespace. Truncate this to its basename, and put the absolute path in // the description. nsAutoCString absPath; absPath.Append(aPath); absPath.StripChars(" "); nsAutoCString basename; GetBasename(absPath, basename); if (basename.EqualsLiteral("[heap]")) { aName.Append("anonymous/anonymous, within brk()"); aDesc.Append("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 node corresponds to " "'[heap]' in /proc/self/smaps."); } else if (basename.EqualsLiteral("[stack]")) { aName.Append("main thread's stack"); aDesc.Append("The stack size of the process's main thread. This node " "corresponds to '[stack]' in /proc/self/smaps."); } else if (basename.EqualsLiteral("[vdso]")) { aName.Append("vdso"); aDesc.Append("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."); } else if (!basename.IsEmpty()) { nsAutoCString dirname; 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.Append("shared-libraries/"); if ((!mLibxulDir.IsEmpty() && dirname.Equals(mLibxulDir)) || mMozillaLibraries.Contains(basename)) { aName.Append("shared-libraries-mozilla/"); } else { aName.Append("shared-libraries-other/"); } } else { aName.Append("other-files/"); if (EndsWithLiteral(basename, ".xpi")) { aName.Append("extensions/"); } else if (dirname.Find("/fontconfig") != -1) { aName.Append("fontconfig/"); } } aName.Append(basename); aDesc.Append(absPath); } else { aName.Append("anonymous/anonymous, outside brk()"); aDesc.Append("Memory in anonymous mappings outside the boundaries defined " "by brk() / sbrk()."); } aName.Append(" ["); aName.Append(aPerms); aName.Append("]"); // Modify the description to include an explanation of the permissions. aDesc.Append(" ("); if (strstr(aPerms, "rw")) { aDesc.Append("read/write, "); } else if (strchr(aPerms, 'r')) { aDesc.Append("read-only, "); } else if (strchr(aPerms, 'w')) { aDesc.Append("write-only, "); } else { aDesc.Append("not readable, not writable, "); } if (strchr(aPerms, 'x')) { aDesc.Append("executable, "); } else { aDesc.Append("not executable, "); } if (strchr(aPerms, 's')) { aDesc.Append("shared"); } else if (strchr(aPerms, 'p')) { aDesc.Append("private"); } else { aDesc.Append("not shared or private??"); } aDesc.Append(")"); } nsresult MapsReporter::ParseMapBody( FILE *aFile, const nsACString &aName, const nsACString &aDescription, nsIMemoryMultiReporterCallback *aCb, nsISupports *aClosure, CategoriesSeen *aCategoriesSeen) { MOZ_STATIC_ASSERT(sizeof(long long) == sizeof(int64_t), "size of (long long) is expected to match (int64_t)"); const int argCount = 2; char desc[1025]; unsigned long long size; if (fscanf(aFile, "%1024[a-zA-Z_]: %llu kB\n", desc, &size) != argCount) { return NS_ERROR_FAILURE; } // Don't report nodes with size 0. if (size == 0) return NS_OK; const char* category; if (strcmp(desc, "Size") == 0) { category = "size"; aCategoriesSeen->mSeenSize = true; } else if (strcmp(desc, "Rss") == 0) { category = "rss"; aCategoriesSeen->mSeenRss = true; } else if (strcmp(desc, "Pss") == 0) { category = "pss"; aCategoriesSeen->mSeenPss = true; } else if (strcmp(desc, "Swap") == 0) { category = "swap"; aCategoriesSeen->mSeenSwap = true; } else { // Don't report this category. return NS_OK; } nsAutoCString path; path.Append(category); path.Append("/"); path.Append(aName); nsresult rv; rv = aCb->Callback(NS_LITERAL_CSTRING(""), path, nsIMemoryReporter::KIND_NONHEAP, nsIMemoryReporter::UNITS_BYTES, int64_t(size) * 1024, // convert from kB to bytes aDescription, aClosure); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } static nsresult GetUSS(int64_t *n) { // You might be tempted to calculate USS by subtracting the "shared" value // from the "resident" value in /proc//statm. But at least on Linux, // statm's "shared" value actually counts pages backed by files, which has // little to do with whether the pages are actually shared. smaps on the // other hand appears to give us the correct information. // // We could calculate this data during the smaps multi-reporter, but the // overhead of the smaps reporter is considerable (we don't even run the // smaps reporter in normal about:memory operation). Hopefully this // implementation is fast enough not to matter. *n = 0; FILE *f = fopen("/proc/self/smaps", "r"); NS_ENSURE_STATE(f); int64_t total = 0; char line[256]; while(fgets(line, sizeof(line), f)) { long long val = 0; if(sscanf(line, "Private_Dirty: %lld kB", &val) == 1 || sscanf(line, "Private_Clean: %lld kB", &val) == 1) { total += val * 1024; // convert from kB to bytes } } *n = total; fclose(f); return NS_OK; } NS_FALLIBLE_MEMORY_REPORTER_IMPLEMENT(USS, "resident-unique", KIND_OTHER, UNITS_BYTES, GetUSS, "Memory mapped by the process that is present in physical memory and not " "shared with any other processes. This is also known as the process's " "unique set size (USS). This is the amount of RAM we'd expect to be freed " "if we closed this process.") void Init() { nsCOMPtr reporter = new MapsReporter(); NS_RegisterMemoryMultiReporter(reporter); NS_RegisterMemoryReporter(new NS_MEMORY_REPORTER_NAME(USS)); } } // namespace MapsMemoryReporter } // namespace mozilla