michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=8 sts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/AvailableMemoryTracker.h" michael@0: michael@0: #if defined(XP_WIN) michael@0: #include "prinrval.h" michael@0: #include "prenv.h" michael@0: #include "nsIMemoryReporter.h" michael@0: #include "nsMemoryPressure.h" michael@0: #endif michael@0: michael@0: #include "nsIObserver.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsIRunnable.h" michael@0: #include "nsISupports.h" michael@0: #include "nsThreadUtils.h" michael@0: michael@0: #include "mozilla/Preferences.h" michael@0: #include "mozilla/Services.h" michael@0: michael@0: #if defined(XP_WIN) michael@0: # include "nsWindowsDllInterceptor.h" michael@0: # include michael@0: #endif michael@0: michael@0: #if defined(MOZ_MEMORY) michael@0: # include "mozmemory.h" michael@0: #endif // MOZ_MEMORY michael@0: michael@0: using namespace mozilla; michael@0: michael@0: namespace { michael@0: michael@0: #if defined(XP_WIN) michael@0: michael@0: // We don't want our diagnostic functions to call malloc, because that could michael@0: // call VirtualAlloc, and we'd end up back in here! So here are a few simple michael@0: // debugging macros (modeled on jemalloc's), which hopefully won't allocate. michael@0: michael@0: // #define LOGGING_ENABLED michael@0: michael@0: #ifdef LOGGING_ENABLED michael@0: michael@0: #define LOG(msg) \ michael@0: do { \ michael@0: safe_write(msg); \ michael@0: safe_write("\n"); \ michael@0: } while(0) michael@0: michael@0: #define LOG2(m1, m2) \ michael@0: do { \ michael@0: safe_write(m1); \ michael@0: safe_write(m2); \ michael@0: safe_write("\n"); \ michael@0: } while(0) michael@0: michael@0: #define LOG3(m1, m2, m3) \ michael@0: do { \ michael@0: safe_write(m1); \ michael@0: safe_write(m2); \ michael@0: safe_write(m3); \ michael@0: safe_write("\n"); \ michael@0: } while(0) michael@0: michael@0: #define LOG4(m1, m2, m3, m4) \ michael@0: do { \ michael@0: safe_write(m1); \ michael@0: safe_write(m2); \ michael@0: safe_write(m3); \ michael@0: safe_write(m4); \ michael@0: safe_write("\n"); \ michael@0: } while(0) michael@0: michael@0: #else michael@0: michael@0: #define LOG(msg) michael@0: #define LOG2(m1, m2) michael@0: #define LOG3(m1, m2, m3) michael@0: #define LOG4(m1, m2, m3, m4) michael@0: michael@0: #endif michael@0: michael@0: void safe_write(const char *a) michael@0: { michael@0: // Well, puts isn't exactly "safe", but at least it doesn't call malloc... michael@0: fputs(a, stdout); michael@0: } michael@0: michael@0: void safe_write(uint64_t x) michael@0: { michael@0: // 2^64 is 20 decimal digits. michael@0: const unsigned int max_len = 21; michael@0: char buf[max_len]; michael@0: buf[max_len - 1] = '\0'; michael@0: michael@0: uint32_t i; michael@0: for (i = max_len - 2; i < max_len && x > 0; i--) michael@0: { michael@0: buf[i] = "0123456789"[x % 10]; michael@0: x /= 10; michael@0: } michael@0: michael@0: safe_write(&buf[i + 1]); michael@0: } michael@0: michael@0: #ifdef DEBUG michael@0: #define DEBUG_WARN_IF_FALSE(cond, msg) \ michael@0: do { \ michael@0: if (!(cond)) { \ michael@0: safe_write(__FILE__); \ michael@0: safe_write(":"); \ michael@0: safe_write(__LINE__); \ michael@0: safe_write(" "); \ michael@0: safe_write(msg); \ michael@0: safe_write("\n"); \ michael@0: } \ michael@0: } while(0) michael@0: #else michael@0: #define DEBUG_WARN_IF_FALSE(cond, msg) michael@0: #endif michael@0: michael@0: uint32_t sLowVirtualMemoryThreshold = 0; michael@0: uint32_t sLowCommitSpaceThreshold = 0; michael@0: uint32_t sLowPhysicalMemoryThreshold = 0; michael@0: uint32_t sLowMemoryNotificationIntervalMS = 0; michael@0: michael@0: Atomic sNumLowVirtualMemEvents; michael@0: Atomic sNumLowCommitSpaceEvents; michael@0: Atomic sNumLowPhysicalMemEvents; michael@0: michael@0: WindowsDllInterceptor sKernel32Intercept; michael@0: WindowsDllInterceptor sGdi32Intercept; michael@0: michael@0: // Has Init() been called? michael@0: bool sInitialized = false; michael@0: michael@0: // Has Activate() been called? The hooks don't do anything until this happens. michael@0: bool sHooksActive = false; michael@0: michael@0: // Alas, we'd like to use mozilla::TimeStamp, but we can't, because it acquires michael@0: // a lock! michael@0: volatile bool sHasScheduledOneLowMemoryNotification = false; michael@0: volatile PRIntervalTime sLastLowMemoryNotificationTime; michael@0: michael@0: // These are function pointers to the functions we wrap in Init(). michael@0: michael@0: void* (WINAPI *sVirtualAllocOrig) michael@0: (LPVOID aAddress, SIZE_T aSize, DWORD aAllocationType, DWORD aProtect); michael@0: michael@0: void* (WINAPI *sMapViewOfFileOrig) michael@0: (HANDLE aFileMappingObject, DWORD aDesiredAccess, michael@0: DWORD aFileOffsetHigh, DWORD aFileOffsetLow, michael@0: SIZE_T aNumBytesToMap); michael@0: michael@0: HBITMAP (WINAPI *sCreateDIBSectionOrig) michael@0: (HDC aDC, const BITMAPINFO *aBitmapInfo, michael@0: UINT aUsage, VOID **aBits, michael@0: HANDLE aSection, DWORD aOffset); michael@0: michael@0: /** michael@0: * Fire a memory pressure event if it's been long enough since the last one we michael@0: * fired. michael@0: */ michael@0: bool MaybeScheduleMemoryPressureEvent() michael@0: { michael@0: // If this interval rolls over, we may fire an extra memory pressure michael@0: // event, but that's not a big deal. michael@0: PRIntervalTime interval = PR_IntervalNow() - sLastLowMemoryNotificationTime; michael@0: if (sHasScheduledOneLowMemoryNotification && michael@0: PR_IntervalToMilliseconds(interval) < sLowMemoryNotificationIntervalMS) { michael@0: michael@0: LOG("Not scheduling low physical memory notification, " michael@0: "because not enough time has elapsed since last one."); michael@0: return false; michael@0: } michael@0: michael@0: // There's a bit of a race condition here, since an interval may be a michael@0: // 64-bit number, and 64-bit writes aren't atomic on x86-32. But let's michael@0: // not worry about it -- the races only happen when we're already michael@0: // experiencing memory pressure and firing notifications, so the worst michael@0: // thing that can happen is that we fire two notifications when we michael@0: // should have fired only one. michael@0: sHasScheduledOneLowMemoryNotification = true; michael@0: sLastLowMemoryNotificationTime = PR_IntervalNow(); michael@0: michael@0: LOG("Scheduling memory pressure notification."); michael@0: NS_DispatchEventualMemoryPressure(MemPressure_New); michael@0: return true; michael@0: } michael@0: michael@0: void CheckMemAvailable() michael@0: { michael@0: if (!sHooksActive) { michael@0: return; michael@0: } michael@0: michael@0: MEMORYSTATUSEX stat; michael@0: stat.dwLength = sizeof(stat); michael@0: bool success = GlobalMemoryStatusEx(&stat); michael@0: michael@0: DEBUG_WARN_IF_FALSE(success, "GlobalMemoryStatusEx failed."); michael@0: michael@0: if (success) michael@0: { michael@0: // sLowVirtualMemoryThreshold is in MB, but ullAvailVirtual is in bytes. michael@0: if (stat.ullAvailVirtual < sLowVirtualMemoryThreshold * 1024 * 1024) { michael@0: // If we're running low on virtual memory, unconditionally schedule the michael@0: // notification. We'll probably crash if we run out of virtual memory, michael@0: // so don't worry about firing this notification too often. michael@0: LOG("Detected low virtual memory."); michael@0: ++sNumLowVirtualMemEvents; michael@0: NS_DispatchEventualMemoryPressure(MemPressure_New); michael@0: } michael@0: else if (stat.ullAvailPageFile < sLowCommitSpaceThreshold * 1024 * 1024) { michael@0: LOG("Detected low available page file space."); michael@0: if (MaybeScheduleMemoryPressureEvent()) { michael@0: ++sNumLowCommitSpaceEvents; michael@0: } michael@0: } michael@0: else if (stat.ullAvailPhys < sLowPhysicalMemoryThreshold * 1024 * 1024) { michael@0: LOG("Detected low physical memory."); michael@0: if (MaybeScheduleMemoryPressureEvent()) { michael@0: ++sNumLowPhysicalMemEvents; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: LPVOID WINAPI michael@0: VirtualAllocHook(LPVOID aAddress, SIZE_T aSize, michael@0: DWORD aAllocationType, michael@0: DWORD aProtect) michael@0: { michael@0: // It's tempting to see whether we have enough free virtual address space for michael@0: // this allocation and, if we don't, synchronously fire a low-memory michael@0: // notification to free some before we allocate. michael@0: // michael@0: // Unfortunately that doesn't work, principally because code doesn't expect a michael@0: // call to malloc could trigger a GC (or call into the other routines which michael@0: // are triggered by a low-memory notification). michael@0: // michael@0: // I think the best we can do here is try to allocate the memory and check michael@0: // afterwards how much free virtual address space we have. If we're running michael@0: // low, we schedule a low-memory notification to run as soon as possible. michael@0: michael@0: LPVOID result = sVirtualAllocOrig(aAddress, aSize, aAllocationType, aProtect); michael@0: michael@0: // Don't call CheckMemAvailable for MEM_RESERVE if we're not tracking low michael@0: // virtual memory. Similarly, don't call CheckMemAvailable for MEM_COMMIT if michael@0: // we're not tracking low physical memory. michael@0: if ((sLowVirtualMemoryThreshold != 0 && aAllocationType & MEM_RESERVE) || michael@0: (sLowPhysicalMemoryThreshold != 0 && aAllocationType & MEM_COMMIT)) { michael@0: LOG3("VirtualAllocHook(size=", aSize, ")"); michael@0: CheckMemAvailable(); michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: LPVOID WINAPI michael@0: MapViewOfFileHook(HANDLE aFileMappingObject, michael@0: DWORD aDesiredAccess, michael@0: DWORD aFileOffsetHigh, michael@0: DWORD aFileOffsetLow, michael@0: SIZE_T aNumBytesToMap) michael@0: { michael@0: LPVOID result = sMapViewOfFileOrig(aFileMappingObject, aDesiredAccess, michael@0: aFileOffsetHigh, aFileOffsetLow, michael@0: aNumBytesToMap); michael@0: LOG("MapViewOfFileHook"); michael@0: CheckMemAvailable(); michael@0: return result; michael@0: } michael@0: michael@0: HBITMAP WINAPI michael@0: CreateDIBSectionHook(HDC aDC, michael@0: const BITMAPINFO *aBitmapInfo, michael@0: UINT aUsage, michael@0: VOID **aBits, michael@0: HANDLE aSection, michael@0: DWORD aOffset) michael@0: { michael@0: // There are a lot of calls to CreateDIBSection, so we make some effort not michael@0: // to CheckMemAvailable() for calls to CreateDIBSection which allocate only michael@0: // a small amount of memory. michael@0: michael@0: // If aSection is non-null, CreateDIBSection won't allocate any new memory. michael@0: bool doCheck = false; michael@0: if (sHooksActive && !aSection && aBitmapInfo) { michael@0: uint16_t bitCount = aBitmapInfo->bmiHeader.biBitCount; michael@0: if (bitCount == 0) { michael@0: // MSDN says bitCount == 0 means that it figures out how many bits each michael@0: // pixel gets by examining the corresponding JPEG or PNG data. We'll just michael@0: // assume the worst. michael@0: bitCount = 32; michael@0: } michael@0: michael@0: // |size| contains the expected allocation size in *bits*. Height may be michael@0: // negative (indicating the direction the DIB is drawn in), so we take the michael@0: // absolute value. michael@0: int64_t size = bitCount * aBitmapInfo->bmiHeader.biWidth * michael@0: aBitmapInfo->bmiHeader.biHeight; michael@0: if (size < 0) michael@0: size *= -1; michael@0: michael@0: // If we're allocating more than 1MB, check how much memory is left after michael@0: // the allocation. michael@0: if (size > 1024 * 1024 * 8) { michael@0: LOG3("CreateDIBSectionHook: Large allocation (size=", size, ")"); michael@0: doCheck = true; michael@0: } michael@0: } michael@0: michael@0: HBITMAP result = sCreateDIBSectionOrig(aDC, aBitmapInfo, aUsage, aBits, michael@0: aSection, aOffset); michael@0: michael@0: if (doCheck) { michael@0: CheckMemAvailable(); michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: static int64_t michael@0: LowMemoryEventsVirtualDistinguishedAmount() michael@0: { michael@0: return sNumLowVirtualMemEvents; michael@0: } michael@0: michael@0: static int64_t michael@0: LowMemoryEventsPhysicalDistinguishedAmount() michael@0: { michael@0: return sNumLowPhysicalMemEvents; michael@0: } michael@0: michael@0: class LowEventsReporter MOZ_FINAL : public nsIMemoryReporter michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: michael@0: NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport, michael@0: nsISupports* aData) michael@0: { michael@0: nsresult rv; michael@0: michael@0: // We only do virtual-memory tracking on 32-bit builds. michael@0: if (sizeof(void*) == 4) { michael@0: rv = MOZ_COLLECT_REPORT( michael@0: "low-memory-events/virtual", KIND_OTHER, UNITS_COUNT_CUMULATIVE, michael@0: LowMemoryEventsVirtualDistinguishedAmount(), michael@0: "Number of low-virtual-memory events fired since startup. We fire such an " michael@0: "event if we notice there is less than memory.low_virtual_mem_threshold_mb of " michael@0: "virtual address space available (if zero, this behavior is disabled). The " michael@0: "process will probably crash if it runs out of virtual address space, so " michael@0: "this event is dire."); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: rv = MOZ_COLLECT_REPORT( michael@0: "low-commit-space-events", KIND_OTHER, UNITS_COUNT_CUMULATIVE, michael@0: sNumLowCommitSpaceEvents, michael@0: "Number of low-commit-space events fired since startup. We fire such an " michael@0: "event if we notice there is less than memory.low_commit_space_threshold_mb of " michael@0: "commit space available (if zero, this behavior is disabled). Windows will " michael@0: "likely kill the process if it runs out of commit space, so this event is " michael@0: "dire."); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = MOZ_COLLECT_REPORT( michael@0: "low-memory-events/physical", KIND_OTHER, UNITS_COUNT_CUMULATIVE, michael@0: LowMemoryEventsPhysicalDistinguishedAmount(), michael@0: "Number of low-physical-memory events fired since startup. We fire such an " michael@0: "event if we notice there is less than memory.low_physical_memory_threshold_mb " michael@0: "of physical memory available (if zero, this behavior is disabled). The " michael@0: "machine will start to page if it runs out of physical memory. This may " michael@0: "cause it to run slowly, but it shouldn't cause it to crash."); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return NS_OK; michael@0: } michael@0: }; michael@0: NS_IMPL_ISUPPORTS(LowEventsReporter, nsIMemoryReporter) michael@0: michael@0: #endif // defined(XP_WIN) michael@0: michael@0: /** michael@0: * This runnable is executed in response to a memory-pressure event; we spin michael@0: * the event-loop when receiving the memory-pressure event in the hope that michael@0: * other observers will synchronously free some memory that we'll be able to michael@0: * purge here. michael@0: */ michael@0: class nsJemallocFreeDirtyPagesRunnable MOZ_FINAL : public nsIRunnable michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: NS_DECL_NSIRUNNABLE michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(nsJemallocFreeDirtyPagesRunnable, nsIRunnable) michael@0: michael@0: NS_IMETHODIMP michael@0: nsJemallocFreeDirtyPagesRunnable::Run() michael@0: { michael@0: MOZ_ASSERT(NS_IsMainThread()); michael@0: michael@0: #if defined(MOZ_MEMORY) michael@0: jemalloc_free_dirty_pages(); michael@0: #endif michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /** michael@0: * The memory pressure watcher is used for listening to memory-pressure events michael@0: * and reacting upon them. We use one instance per process currently only for michael@0: * cleaning up dirty unused pages held by jemalloc. michael@0: */ michael@0: class nsMemoryPressureWatcher MOZ_FINAL : public nsIObserver michael@0: { michael@0: public: michael@0: NS_DECL_ISUPPORTS michael@0: NS_DECL_NSIOBSERVER michael@0: michael@0: void Init(); michael@0: michael@0: private: michael@0: static bool sFreeDirtyPages; michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(nsMemoryPressureWatcher, nsIObserver) michael@0: michael@0: bool nsMemoryPressureWatcher::sFreeDirtyPages = false; michael@0: michael@0: /** michael@0: * Initialize and subscribe to the memory-pressure events. We subscribe to the michael@0: * observer service in this method and not in the constructor because we need michael@0: * to hold a strong reference to 'this' before calling the observer service. michael@0: */ michael@0: void michael@0: nsMemoryPressureWatcher::Init() michael@0: { michael@0: nsCOMPtr os = services::GetObserverService(); michael@0: michael@0: if (os) { michael@0: os->AddObserver(this, "memory-pressure", /* ownsWeak */ false); michael@0: } michael@0: michael@0: Preferences::AddBoolVarCache(&sFreeDirtyPages, "memory.free_dirty_pages", michael@0: false); michael@0: } michael@0: michael@0: /** michael@0: * Reacts to all types of memory-pressure events, launches a runnable to michael@0: * free dirty pages held by jemalloc. michael@0: */ michael@0: NS_IMETHODIMP michael@0: nsMemoryPressureWatcher::Observe(nsISupports *subject, const char *topic, michael@0: const char16_t *data) michael@0: { michael@0: MOZ_ASSERT(!strcmp(topic, "memory-pressure"), "Unknown topic"); michael@0: michael@0: if (sFreeDirtyPages) { michael@0: nsRefPtr runnable = new nsJemallocFreeDirtyPagesRunnable(); michael@0: michael@0: NS_DispatchToMainThread(runnable); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: } // anonymous namespace michael@0: michael@0: namespace mozilla { michael@0: namespace AvailableMemoryTracker { michael@0: michael@0: void Activate() michael@0: { michael@0: #if defined(_M_IX86) && defined(XP_WIN) michael@0: MOZ_ASSERT(sInitialized); michael@0: MOZ_ASSERT(!sHooksActive); michael@0: michael@0: // On 64-bit systems, hardcode sLowVirtualMemoryThreshold to 0 -- we assume michael@0: // we're not going to run out of virtual memory! michael@0: if (sizeof(void*) > 4) { michael@0: sLowVirtualMemoryThreshold = 0; michael@0: } michael@0: else { michael@0: Preferences::AddUintVarCache(&sLowVirtualMemoryThreshold, michael@0: "memory.low_virtual_mem_threshold_mb", 128); michael@0: } michael@0: michael@0: Preferences::AddUintVarCache(&sLowPhysicalMemoryThreshold, michael@0: "memory.low_physical_memory_threshold_mb", 0); michael@0: Preferences::AddUintVarCache(&sLowCommitSpaceThreshold, michael@0: "memory.low_commit_space_threshold_mb", 128); michael@0: Preferences::AddUintVarCache(&sLowMemoryNotificationIntervalMS, michael@0: "memory.low_memory_notification_interval_ms", 10000); michael@0: michael@0: RegisterStrongMemoryReporter(new LowEventsReporter()); michael@0: RegisterLowMemoryEventsVirtualDistinguishedAmount(LowMemoryEventsVirtualDistinguishedAmount); michael@0: RegisterLowMemoryEventsPhysicalDistinguishedAmount(LowMemoryEventsPhysicalDistinguishedAmount); michael@0: sHooksActive = true; michael@0: #endif michael@0: michael@0: // This object is held alive by the observer service. michael@0: nsRefPtr watcher = new nsMemoryPressureWatcher(); michael@0: watcher->Init(); michael@0: } michael@0: michael@0: void Init() michael@0: { michael@0: // Do nothing on x86-64, because nsWindowsDllInterceptor is not thread-safe michael@0: // on 64-bit. (On 32-bit, it's probably thread-safe.) Even if we run Init() michael@0: // before any other of our threads are running, another process may have michael@0: // started a remote thread which could call VirtualAlloc! michael@0: // michael@0: // Moreover, the benefit of this code is less clear when we're a 64-bit michael@0: // process, because we aren't going to run out of virtual memory, and the michael@0: // system is likely to have a fair bit of physical memory. michael@0: michael@0: #if defined(_M_IX86) && defined(XP_WIN) michael@0: // Don't register the hooks if we're a build instrumented for PGO: If we're michael@0: // an instrumented build, the compiler adds function calls all over the place michael@0: // which may call VirtualAlloc; this makes it hard to prevent michael@0: // VirtualAllocHook from reentering itself. michael@0: if (!PR_GetEnv("MOZ_PGO_INSTRUMENTED")) { michael@0: sKernel32Intercept.Init("Kernel32.dll"); michael@0: sKernel32Intercept.AddHook("VirtualAlloc", michael@0: reinterpret_cast(VirtualAllocHook), michael@0: (void**) &sVirtualAllocOrig); michael@0: sKernel32Intercept.AddHook("MapViewOfFile", michael@0: reinterpret_cast(MapViewOfFileHook), michael@0: (void**) &sMapViewOfFileOrig); michael@0: michael@0: sGdi32Intercept.Init("Gdi32.dll"); michael@0: sGdi32Intercept.AddHook("CreateDIBSection", michael@0: reinterpret_cast(CreateDIBSectionHook), michael@0: (void**) &sCreateDIBSectionOrig); michael@0: } michael@0: michael@0: sInitialized = true; michael@0: #endif michael@0: } michael@0: michael@0: } // namespace AvailableMemoryTracker michael@0: } // namespace mozilla