/* -*- 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/nsMemoryInfoDumper.h" #include "nsDumpUtils.h" #include "mozilla/unused.h" #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/ContentChild.h" #include "nsIConsoleService.h" #include "nsICycleCollectorListener.h" #include "nsIMemoryReporter.h" #include "nsDirectoryServiceDefs.h" #include "nsGZFileWriter.h" #include "nsJSEnvironment.h" #include "nsPrintfCString.h" #include "nsISimpleEnumerator.h" #include "nsServiceManagerUtils.h" #include "nsIFile.h" #ifdef XP_WIN #include #define getpid _getpid #else #include #endif #if defined(XP_LINUX) || defined(__FreeBSD__) || defined(XP_MACOSX) #define MOZ_SUPPORTS_FIFO 1 #endif #if defined(XP_LINUX) || defined(__FreeBSD__) #define MOZ_SUPPORTS_RT_SIGNALS 1 #endif #if defined(MOZ_SUPPORTS_RT_SIGNALS) #include #include #include #endif #if defined(MOZ_SUPPORTS_FIFO) #include "mozilla/Preferences.h" #endif using namespace mozilla; using namespace mozilla::dom; namespace { class DumpMemoryInfoToTempDirRunnable : public nsRunnable { public: DumpMemoryInfoToTempDirRunnable(const nsAString& aIdentifier, bool aMinimizeMemoryUsage) : mIdentifier(aIdentifier) , mMinimizeMemoryUsage(aMinimizeMemoryUsage) {} NS_IMETHOD Run() { nsCOMPtr dumper = do_GetService("@mozilla.org/memory-info-dumper;1"); dumper->DumpMemoryInfoToTempDir(mIdentifier, mMinimizeMemoryUsage); return NS_OK; } private: const nsString mIdentifier; const bool mMinimizeMemoryUsage; }; class GCAndCCLogDumpRunnable : public nsRunnable { public: GCAndCCLogDumpRunnable(const nsAString& aIdentifier, bool aDumpAllTraces, bool aDumpChildProcesses) : mIdentifier(aIdentifier) , mDumpAllTraces(aDumpAllTraces) , mDumpChildProcesses(aDumpChildProcesses) {} NS_IMETHOD Run() { nsCOMPtr dumper = do_GetService("@mozilla.org/memory-info-dumper;1"); nsString ccLogPath, gcLogPath; dumper->DumpGCAndCCLogsToFile(mIdentifier, mDumpAllTraces, mDumpChildProcesses, gcLogPath, ccLogPath); return NS_OK; } private: const nsString mIdentifier; const bool mDumpAllTraces; const bool mDumpChildProcesses; }; } // anonymous namespace #if defined(MOZ_SUPPORTS_RT_SIGNALS) // { namespace { /* * The following code supports dumping about:memory upon receiving a signal. * * We listen for the following signals: * * - SIGRTMIN: Dump our memory reporters (and those of our child * processes), * - SIGRTMIN + 1: Dump our memory reporters (and those of our child * processes) after minimizing memory usage, and * - SIGRTMIN + 2: Dump the GC and CC logs in this and our child processes. * * When we receive one of these signals, we write the signal number to a pipe. * The IO thread then notices that the pipe has been written to, and kicks off * the appropriate task on the main thread. * * This scheme is similar to using signalfd(), except it's portable and it * doesn't require the use of sigprocmask, which is problematic because it * masks signals received by child processes. * * In theory, we could use Chromium's MessageLoopForIO::CatchSignal() for this. * But that uses libevent, which does not handle the realtime signals (bug * 794074). */ // It turns out that at least on some systems, SIGRTMIN is not a compile-time // constant, so these have to be set at runtime. static uint8_t sDumpAboutMemorySignum; // SIGRTMIN static uint8_t sDumpAboutMemoryAfterMMUSignum; // SIGRTMIN + 1 static uint8_t sGCAndCCDumpSignum; // SIGRTMIN + 2 void doMemoryReport(const uint8_t recvSig) { // Dump our memory reports (but run this on the main thread!). bool doMMUFirst = recvSig == sDumpAboutMemoryAfterMMUSignum; LOG("SignalWatcher(sig %d) dispatching memory report runnable.", recvSig); nsRefPtr runnable = new DumpMemoryInfoToTempDirRunnable(/* identifier = */ EmptyString(), doMMUFirst); NS_DispatchToMainThread(runnable); } void doGCCCDump(const uint8_t recvSig) { LOG("SignalWatcher(sig %d) dispatching GC/CC log runnable.", recvSig); // Dump GC and CC logs (from the main thread). nsRefPtr runnable = new GCAndCCLogDumpRunnable( /* identifier = */ EmptyString(), /* allTraces = */ true, /* dumpChildProcesses = */ true); NS_DispatchToMainThread(runnable); } } // anonymous namespace #endif // MOZ_SUPPORTS_RT_SIGNALS } #if defined(MOZ_SUPPORTS_FIFO) // { namespace { void doMemoryReport(const nsCString& inputStr) { bool doMMUMemoryReport = inputStr == NS_LITERAL_CSTRING("minimize memory report"); LOG("FifoWatcher(command:%s) dispatching memory report runnable.", inputStr.get()); nsRefPtr runnable = new DumpMemoryInfoToTempDirRunnable(/* identifier = */ EmptyString(), doMMUMemoryReport); NS_DispatchToMainThread(runnable); } void doGCCCDump(const nsCString& inputStr) { bool doAllTracesGCCCDump = inputStr == NS_LITERAL_CSTRING("gc log"); LOG("FifoWatcher(command:%s) dispatching GC/CC log runnable.", inputStr.get()); nsRefPtr runnable = new GCAndCCLogDumpRunnable(/* identifier = */ EmptyString(), doAllTracesGCCCDump, /* dumpChildProcesses = */ true); NS_DispatchToMainThread(runnable); } bool SetupFifo() { static bool fifoCallbacksRegistered = false; if (!FifoWatcher::MaybeCreate()) { return false; } MOZ_ASSERT(!fifoCallbacksRegistered, "FifoWatcher callbacks should be registered only once"); FifoWatcher* fw = FifoWatcher::GetSingleton(); // Dump our memory reports (but run this on the main thread!). fw->RegisterCallback(NS_LITERAL_CSTRING("memory report"), doMemoryReport); fw->RegisterCallback(NS_LITERAL_CSTRING("minimize memory report"), doMemoryReport); // Dump GC and CC logs (from the main thread). fw->RegisterCallback(NS_LITERAL_CSTRING("gc log"), doGCCCDump); fw->RegisterCallback(NS_LITERAL_CSTRING("abbreviated gc log"), doGCCCDump); fifoCallbacksRegistered = true; return true; } void OnFifoEnabledChange(const char* /*unused*/, void* /*unused*/) { LOG("%s changed", FifoWatcher::kPrefName); if (SetupFifo()) { Preferences::UnregisterCallback(OnFifoEnabledChange, FifoWatcher::kPrefName, nullptr); } } } // anonymous namespace #endif // MOZ_SUPPORTS_FIFO } NS_IMPL_ISUPPORTS(nsMemoryInfoDumper, nsIMemoryInfoDumper) nsMemoryInfoDumper::nsMemoryInfoDumper() { } nsMemoryInfoDumper::~nsMemoryInfoDumper() { } /* static */ void nsMemoryInfoDumper::Initialize() { #if defined(MOZ_SUPPORTS_RT_SIGNALS) SignalPipeWatcher* sw = SignalPipeWatcher::GetSingleton(); // Dump memory reporters (and those of our child processes) sDumpAboutMemorySignum = SIGRTMIN; sw->RegisterCallback(sDumpAboutMemorySignum, doMemoryReport); // Dump our memory reporters after minimizing memory usage sDumpAboutMemoryAfterMMUSignum = SIGRTMIN + 1; sw->RegisterCallback(sDumpAboutMemoryAfterMMUSignum, doMemoryReport); // Dump the GC and CC logs in this and our child processes. sGCAndCCDumpSignum = SIGRTMIN + 2; sw->RegisterCallback(sGCAndCCDumpSignum, doGCCCDump); #endif #if defined(MOZ_SUPPORTS_FIFO) if (!SetupFifo()) { // NB: This gets loaded early enough that it's possible there is a user pref // set to enable the fifo watcher that has not been loaded yet. Register // to attempt to initialize if the fifo watcher becomes enabled by // a user pref. Preferences::RegisterCallback(OnFifoEnabledChange, FifoWatcher::kPrefName, nullptr); } #endif } static void EnsureNonEmptyIdentifier(nsAString& aIdentifier) { if (!aIdentifier.IsEmpty()) { return; } // If the identifier is empty, set it to the number of whole seconds since the // epoch. This identifier will appear in the files that this process // generates and also the files generated by this process's children, allowing // us to identify which files are from the same memory report request. aIdentifier.AppendInt(static_cast(PR_Now()) / 1000000); } NS_IMETHODIMP nsMemoryInfoDumper::DumpGCAndCCLogsToFile(const nsAString& aIdentifier, bool aDumpAllTraces, bool aDumpChildProcesses, nsAString& aGCLogPath, nsAString& aCCLogPath) { nsString identifier(aIdentifier); EnsureNonEmptyIdentifier(identifier); if (aDumpChildProcesses) { nsTArray children; ContentParent::GetAll(children); for (uint32_t i = 0; i < children.Length(); i++) { unused << children[i]->SendDumpGCAndCCLogsToFile( identifier, aDumpAllTraces, aDumpChildProcesses); } } nsCOMPtr logger = do_CreateInstance("@mozilla.org/cycle-collector-logger;1"); logger->SetFilenameIdentifier(identifier); if (aDumpAllTraces) { nsCOMPtr allTracesLogger; logger->AllTraces(getter_AddRefs(allTracesLogger)); logger = allTracesLogger; } nsJSContext::CycleCollectNow(logger); logger->GetGcLogPath(aGCLogPath); logger->GetCcLogPath(aCCLogPath); return NS_OK; } namespace mozilla { #define DUMP(o, s) \ do { \ nsresult rv = (o)->Write(s); \ if (NS_WARN_IF(NS_FAILED(rv))) \ return rv; \ } while (0) class DumpReportCallback MOZ_FINAL : public nsIHandleReportCallback { public: NS_DECL_ISUPPORTS DumpReportCallback(nsGZFileWriter* aWriter) : mIsFirst(true) , mWriter(aWriter) {} NS_IMETHOD Callback(const nsACString &aProcess, const nsACString &aPath, int32_t aKind, int32_t aUnits, int64_t aAmount, const nsACString &aDescription, nsISupports *aData) { if (mIsFirst) { DUMP(mWriter, "["); mIsFirst = false; } else { DUMP(mWriter, ","); } nsAutoCString process; if (aProcess.IsEmpty()) { // If the process is empty, the report originated with the process doing // the dumping. In that case, generate the process identifier, which is of // the form "$PROCESS_NAME (pid $PID)", or just "(pid $PID)" if we don't // have a process name. If we're the main process, we let $PROCESS_NAME be // "Main Process". if (XRE_GetProcessType() == GeckoProcessType_Default) { // We're the main process. process.AssignLiteral("Main Process"); } else if (ContentChild *cc = ContentChild::GetSingleton()) { // Try to get the process name from ContentChild. cc->GetProcessName(process); } ContentChild::AppendProcessId(process); } else { // Otherwise, the report originated with another process and already has a // process name. Just use that. process = aProcess; } DUMP(mWriter, "\n {\"process\": \""); DUMP(mWriter, process); DUMP(mWriter, "\", \"path\": \""); nsCString path(aPath); path.ReplaceSubstring("\\", "\\\\"); /* --> \\ */ path.ReplaceSubstring("\"", "\\\""); // " --> \" DUMP(mWriter, path); DUMP(mWriter, "\", \"kind\": "); DUMP(mWriter, nsPrintfCString("%d", aKind)); DUMP(mWriter, ", \"units\": "); DUMP(mWriter, nsPrintfCString("%d", aUnits)); DUMP(mWriter, ", \"amount\": "); DUMP(mWriter, nsPrintfCString("%lld", aAmount)); nsCString description(aDescription); description.ReplaceSubstring("\\", "\\\\"); /* --> \\ */ description.ReplaceSubstring("\"", "\\\""); // " --> \" description.ReplaceSubstring("\n", "\\n"); // --> \n DUMP(mWriter, ", \"description\": \""); DUMP(mWriter, description); DUMP(mWriter, "\"}"); return NS_OK; } private: bool mIsFirst; nsRefPtr mWriter; }; NS_IMPL_ISUPPORTS(DumpReportCallback, nsIHandleReportCallback) } // namespace mozilla static void MakeFilename(const char *aPrefix, const nsAString &aIdentifier, const char *aSuffix, nsACString &aResult) { aResult = nsPrintfCString("%s-%s-%d.%s", aPrefix, NS_ConvertUTF16toUTF8(aIdentifier).get(), getpid(), aSuffix); } #ifdef MOZ_DMD struct DMDWriteState { static const size_t kBufSize = 4096; char mBuf[kBufSize]; nsRefPtr mGZWriter; DMDWriteState(nsGZFileWriter *aGZWriter) : mGZWriter(aGZWriter) {} }; static void DMDWrite(void* aState, const char* aFmt, va_list ap) { DMDWriteState *state = (DMDWriteState*)aState; vsnprintf(state->mBuf, state->kBufSize, aFmt, ap); unused << state->mGZWriter->Write(state->mBuf); } #endif static nsresult DumpHeader(nsIGZFileWriter* aWriter) { // Increment this number if the format changes. // // This is the first write to the file, and it causes |aWriter| to allocate // over 200 KiB of memory. // DUMP(aWriter, "{\n \"version\": 1,\n"); DUMP(aWriter, " \"hasMozMallocUsableSize\": "); nsCOMPtr mgr = do_GetService("@mozilla.org/memory-reporter-manager;1"); if (NS_WARN_IF(!mgr)) return NS_ERROR_UNEXPECTED; DUMP(aWriter, mgr->GetHasMozMallocUsableSize() ? "true" : "false"); DUMP(aWriter, ",\n"); DUMP(aWriter, " \"reports\": "); return NS_OK; } static nsresult DumpFooter(nsIGZFileWriter* aWriter) { DUMP(aWriter, "\n ]\n}\n"); return NS_OK; } class TempDirMemoryFinishCallback MOZ_FINAL : public nsIFinishReportingCallback { public: NS_DECL_ISUPPORTS TempDirMemoryFinishCallback(nsGZFileWriter *aWriter, nsIFile *aTmpFile, const nsCString &aFilename, const nsString &aIdentifier) : mrWriter(aWriter) , mrTmpFile(aTmpFile) , mrFilename(aFilename) , mIdentifier(aIdentifier) {} NS_IMETHOD Callback(nsISupports *aData); private: nsRefPtr mrWriter; nsCOMPtr mrTmpFile; nsCString mrFilename; nsString mIdentifier; }; NS_IMPL_ISUPPORTS(TempDirMemoryFinishCallback, nsIFinishReportingCallback) NS_IMETHODIMP nsMemoryInfoDumper::DumpMemoryInfoToTempDir(const nsAString& aIdentifier, bool aMinimizeMemoryUsage) { nsString identifier(aIdentifier); EnsureNonEmptyIdentifier(identifier); #ifdef MOZ_DMD // Clear DMD's reportedness state before running the memory reporters, to // avoid spurious twice-reported warnings. dmd::ClearReports(); #endif // Open a new file named something like // // incomplete-memory-report--.json.gz // // in NS_OS_TEMP_DIR for writing. When we're finished writing the report, // we'll rename this file and get rid of the "incomplete-" prefix. // // We do this because we don't want scripts which poll the filesystem // looking for memory report dumps to grab a file before we're finished // writing to it. // Note that |mrFilename| is missing the "incomplete-" prefix; we'll tack // that on in a moment. nsCString mrFilename; // The "unified" indicates that we merge the memory reports from all // processes and write out one file, rather than a separate file for // each process as was the case before bug 946407. This is so that // the get_about_memory.py script in the B2G repository can // determine when it's done waiting for files to appear. MakeFilename("unified-memory-report", identifier, "json.gz", mrFilename); nsCOMPtr mrTmpFile; nsresult rv; // In Android case, this function will open a file named aFilename under // specific folder (/data/local/tmp/memory-reports). Otherwise, it will // open a file named aFilename under "NS_OS_TEMP_DIR". rv = nsDumpUtils::OpenTempFile(NS_LITERAL_CSTRING("incomplete-") + mrFilename, getter_AddRefs(mrTmpFile), NS_LITERAL_CSTRING("memory-reports")); if (NS_WARN_IF(NS_FAILED(rv))) return rv; nsRefPtr mrWriter = new nsGZFileWriter(); rv = mrWriter->Init(mrTmpFile); if (NS_WARN_IF(NS_FAILED(rv))) return rv; // Dump the memory reports to the file. rv = DumpHeader(mrWriter); if (NS_WARN_IF(NS_FAILED(rv))) return rv; // Process reporters. nsCOMPtr mgr = do_GetService("@mozilla.org/memory-reporter-manager;1"); nsRefPtr dumpReport = new DumpReportCallback(mrWriter); nsRefPtr finishReport = new TempDirMemoryFinishCallback(mrWriter, mrTmpFile, mrFilename, identifier); rv = mgr->GetReportsExtended(dumpReport, nullptr, finishReport, nullptr, aMinimizeMemoryUsage, identifier); return rv; } #ifdef MOZ_DMD nsresult nsMemoryInfoDumper::DumpDMD(const nsAString &aIdentifier) { if (!dmd::IsEnabled()) { return NS_OK; } nsresult rv; // Create a filename like dmd--.txt.gz, which will be used // if DMD is enabled. nsCString dmdFilename; MakeFilename("dmd", aIdentifier, "txt.gz", dmdFilename); // Open a new DMD file named |dmdFilename| in NS_OS_TEMP_DIR for writing, // and dump DMD output to it. This must occur after the memory reporters // have been run (above), but before the memory-reports file has been // renamed (so scripts can detect the DMD file, if present). nsCOMPtr dmdFile; rv = nsDumpUtils::OpenTempFile(dmdFilename, getter_AddRefs(dmdFile), NS_LITERAL_CSTRING("memory-reports")); if (NS_WARN_IF(NS_FAILED(rv))) return rv; nsRefPtr dmdWriter = new nsGZFileWriter(); rv = dmdWriter->Init(dmdFile); if (NS_WARN_IF(NS_FAILED(rv))) return rv; // Dump DMD output to the file. DMDWriteState state(dmdWriter); dmd::Writer w(DMDWrite, &state); dmd::Dump(w); rv = dmdWriter->Finish(); NS_WARN_IF(NS_FAILED(rv)); return rv; } #endif // MOZ_DMD NS_IMETHODIMP TempDirMemoryFinishCallback::Callback(nsISupports *aData) { nsresult rv; rv = DumpFooter(mrWriter); if (NS_WARN_IF(NS_FAILED(rv))) return rv; // The call to Finish() deallocates the memory allocated by mrWriter's first // DUMP() call (within DumpProcessMemoryReportsToGZFileWriter()). Because // that memory was live while the memory reporters ran and thus measured by // them -- by "heap-allocated" if nothing else -- we want DMD to see it as // well. So we deliberately don't call Finish() until after DMD finishes. rv = mrWriter->Finish(); if (NS_WARN_IF(NS_FAILED(rv))) return rv; // Rename the memory reports file, now that we're done writing all the files. // Its final name is "memory-report<-identifier>-.json.gz". nsCOMPtr mrFinalFile; rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mrFinalFile)); if (NS_WARN_IF(NS_FAILED(rv))) return rv; #ifdef ANDROID rv = mrFinalFile->AppendNative(NS_LITERAL_CSTRING("memory-reports")); if (NS_WARN_IF(NS_FAILED(rv))) return rv; #endif rv = mrFinalFile->AppendNative(mrFilename); if (NS_WARN_IF(NS_FAILED(rv))) return rv; rv = mrFinalFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); if (NS_WARN_IF(NS_FAILED(rv))) return rv; nsAutoString mrActualFinalFilename; rv = mrFinalFile->GetLeafName(mrActualFinalFilename); if (NS_WARN_IF(NS_FAILED(rv))) return rv; rv = mrTmpFile->MoveTo(/* directory */ nullptr, mrActualFinalFilename); if (NS_WARN_IF(NS_FAILED(rv))) return rv; // Write a message to the console. nsCOMPtr cs = do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv); if (NS_WARN_IF(NS_FAILED(rv))) return rv; nsString path; mrTmpFile->GetPath(path); if (NS_WARN_IF(NS_FAILED(rv))) return rv; nsString msg = NS_LITERAL_STRING( "nsIMemoryInfoDumper dumped reports to "); msg.Append(path); return cs->LogStringMessage(msg.get()); } // This dumps the JSON footer and closes the file, and then calls the given // nsIFinishDumpingCallback. class FinishReportingCallback MOZ_FINAL : public nsIFinishReportingCallback { public: NS_DECL_ISUPPORTS FinishReportingCallback(nsIFinishDumpingCallback* aFinishDumping, nsISupports* aFinishDumpingData) : mFinishDumping(aFinishDumping) , mFinishDumpingData(aFinishDumpingData) {} NS_IMETHOD Callback(nsISupports* aData) { nsCOMPtr writer = do_QueryInterface(aData); NS_ENSURE_TRUE(writer, NS_ERROR_FAILURE); nsresult rv = DumpFooter(writer); NS_ENSURE_SUCCESS(rv, rv); rv = writer->Finish(); NS_ENSURE_SUCCESS(rv, rv); if (!mFinishDumping) { return NS_OK; } return mFinishDumping->Callback(mFinishDumpingData); } private: nsCOMPtr mFinishDumping; nsCOMPtr mFinishDumpingData; }; NS_IMPL_ISUPPORTS(FinishReportingCallback, nsIFinishReportingCallback) NS_IMETHODIMP nsMemoryInfoDumper::DumpMemoryReportsToNamedFile( const nsAString& aFilename, nsIFinishDumpingCallback* aFinishDumping, nsISupports* aFinishDumpingData) { MOZ_ASSERT(!aFilename.IsEmpty()); // Create the file. nsCOMPtr mrFile; nsresult rv = NS_NewLocalFile(aFilename, false, getter_AddRefs(mrFile)); if (NS_WARN_IF(NS_FAILED(rv))) return rv; mrFile->InitWithPath(aFilename); if (NS_WARN_IF(NS_FAILED(rv))) return rv; bool exists; rv = mrFile->Exists(&exists); if (NS_WARN_IF(NS_FAILED(rv))) return rv; if (!exists) { rv = mrFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); if (NS_WARN_IF(NS_FAILED(rv))) return rv; } // Write the memory reports to the file. nsRefPtr mrWriter = new nsGZFileWriter(); rv = mrWriter->Init(mrFile); if (NS_WARN_IF(NS_FAILED(rv))) return rv; rv = DumpHeader(mrWriter); if (NS_WARN_IF(NS_FAILED(rv))) return rv; // Process reports and finish up. nsRefPtr dumpReport = new DumpReportCallback(mrWriter); nsRefPtr finishReporting = new FinishReportingCallback(aFinishDumping, aFinishDumpingData); nsCOMPtr mgr = do_GetService("@mozilla.org/memory-reporter-manager;1"); return mgr->GetReports(dumpReport, nullptr, finishReporting, mrWriter); } #undef DUMP