Line data Source code
1 : /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 : /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 : /* This Source Code Form is subject to the terms of the Mozilla Public
4 : * License, v. 2.0. If a copy of the MPL was not distributed with this
5 : * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 :
7 : #include "mozilla/AvailableMemoryTracker.h"
8 :
9 : #if defined(XP_WIN)
10 : #include "prinrval.h"
11 : #include "prenv.h"
12 : #include "nsIMemoryReporter.h"
13 : #include "nsMemoryPressure.h"
14 : #endif
15 :
16 : #include "nsIObserver.h"
17 : #include "nsIObserverService.h"
18 : #include "nsIRunnable.h"
19 : #include "nsISupports.h"
20 : #include "nsThreadUtils.h"
21 :
22 : #include "mozilla/Preferences.h"
23 : #include "mozilla/Services.h"
24 :
25 : #if defined(XP_WIN)
26 : # include "nsWindowsDllInterceptor.h"
27 : # include <windows.h>
28 : #endif
29 :
30 : #if defined(MOZ_MEMORY)
31 : # include "mozmemory.h"
32 : #endif // MOZ_MEMORY
33 :
34 : using namespace mozilla;
35 :
36 : namespace {
37 :
38 : #if defined(_M_IX86) && defined(XP_WIN)
39 :
40 :
41 : uint32_t sLowVirtualMemoryThreshold = 0;
42 : uint32_t sLowCommitSpaceThreshold = 0;
43 : uint32_t sLowPhysicalMemoryThreshold = 0;
44 : uint32_t sLowMemoryNotificationIntervalMS = 0;
45 :
46 : Atomic<uint32_t> sNumLowVirtualMemEvents;
47 : Atomic<uint32_t> sNumLowCommitSpaceEvents;
48 : Atomic<uint32_t> sNumLowPhysicalMemEvents;
49 :
50 : WindowsDllInterceptor sKernel32Intercept;
51 : WindowsDllInterceptor sGdi32Intercept;
52 :
53 : // Has Init() been called?
54 : bool sInitialized = false;
55 :
56 : // Has Activate() been called? The hooks don't do anything until this happens.
57 : bool sHooksActive = false;
58 :
59 : // Alas, we'd like to use mozilla::TimeStamp, but we can't, because it acquires
60 : // a lock!
61 : volatile bool sHasScheduledOneLowMemoryNotification = false;
62 : volatile PRIntervalTime sLastLowMemoryNotificationTime;
63 :
64 : // These are function pointers to the functions we wrap in Init().
65 :
66 : void* (WINAPI* sVirtualAllocOrig)(LPVOID aAddress, SIZE_T aSize,
67 : DWORD aAllocationType, DWORD aProtect);
68 :
69 : void* (WINAPI* sMapViewOfFileOrig)(HANDLE aFileMappingObject,
70 : DWORD aDesiredAccess, DWORD aFileOffsetHigh,
71 : DWORD aFileOffsetLow, SIZE_T aNumBytesToMap);
72 :
73 : HBITMAP(WINAPI* sCreateDIBSectionOrig)(HDC aDC, const BITMAPINFO* aBitmapInfo,
74 : UINT aUsage, VOID** aBits,
75 : HANDLE aSection, DWORD aOffset);
76 :
77 : /**
78 : * Fire a memory pressure event if it's been long enough since the last one we
79 : * fired.
80 : */
81 : bool
82 : MaybeScheduleMemoryPressureEvent()
83 : {
84 : // If this interval rolls over, we may fire an extra memory pressure
85 : // event, but that's not a big deal.
86 : PRIntervalTime interval = PR_IntervalNow() - sLastLowMemoryNotificationTime;
87 : if (sHasScheduledOneLowMemoryNotification &&
88 : PR_IntervalToMilliseconds(interval) < sLowMemoryNotificationIntervalMS) {
89 :
90 : return false;
91 : }
92 :
93 : // There's a bit of a race condition here, since an interval may be a
94 : // 64-bit number, and 64-bit writes aren't atomic on x86-32. But let's
95 : // not worry about it -- the races only happen when we're already
96 : // experiencing memory pressure and firing notifications, so the worst
97 : // thing that can happen is that we fire two notifications when we
98 : // should have fired only one.
99 : sHasScheduledOneLowMemoryNotification = true;
100 : sLastLowMemoryNotificationTime = PR_IntervalNow();
101 :
102 : NS_DispatchEventualMemoryPressure(MemPressure_New);
103 : return true;
104 : }
105 :
106 : void
107 : CheckMemAvailable()
108 : {
109 : if (!sHooksActive) {
110 : return;
111 : }
112 :
113 : MEMORYSTATUSEX stat;
114 : stat.dwLength = sizeof(stat);
115 : bool success = GlobalMemoryStatusEx(&stat);
116 :
117 : if (success) {
118 : // sLowVirtualMemoryThreshold is in MB, but ullAvailVirtual is in bytes.
119 : if (stat.ullAvailVirtual < sLowVirtualMemoryThreshold * 1024 * 1024) {
120 : // If we're running low on virtual memory, unconditionally schedule the
121 : // notification. We'll probably crash if we run out of virtual memory,
122 : // so don't worry about firing this notification too often.
123 : ++sNumLowVirtualMemEvents;
124 : NS_DispatchEventualMemoryPressure(MemPressure_New);
125 : } else if (stat.ullAvailPageFile < sLowCommitSpaceThreshold * 1024 * 1024) {
126 : if (MaybeScheduleMemoryPressureEvent()) {
127 : ++sNumLowCommitSpaceEvents;
128 : }
129 : } else if (stat.ullAvailPhys < sLowPhysicalMemoryThreshold * 1024 * 1024) {
130 : if (MaybeScheduleMemoryPressureEvent()) {
131 : ++sNumLowPhysicalMemEvents;
132 : }
133 : }
134 : }
135 : }
136 :
137 : LPVOID WINAPI
138 : VirtualAllocHook(LPVOID aAddress, SIZE_T aSize,
139 : DWORD aAllocationType,
140 : DWORD aProtect)
141 : {
142 : // It's tempting to see whether we have enough free virtual address space for
143 : // this allocation and, if we don't, synchronously fire a low-memory
144 : // notification to free some before we allocate.
145 : //
146 : // Unfortunately that doesn't work, principally because code doesn't expect a
147 : // call to malloc could trigger a GC (or call into the other routines which
148 : // are triggered by a low-memory notification).
149 : //
150 : // I think the best we can do here is try to allocate the memory and check
151 : // afterwards how much free virtual address space we have. If we're running
152 : // low, we schedule a low-memory notification to run as soon as possible.
153 :
154 : LPVOID result = sVirtualAllocOrig(aAddress, aSize, aAllocationType, aProtect);
155 :
156 : // Don't call CheckMemAvailable for MEM_RESERVE if we're not tracking low
157 : // virtual memory. Similarly, don't call CheckMemAvailable for MEM_COMMIT if
158 : // we're not tracking low physical memory.
159 : if ((sLowVirtualMemoryThreshold != 0 && aAllocationType & MEM_RESERVE) ||
160 : (sLowPhysicalMemoryThreshold != 0 && aAllocationType & MEM_COMMIT)) {
161 : CheckMemAvailable();
162 : }
163 :
164 : return result;
165 : }
166 :
167 : LPVOID WINAPI
168 : MapViewOfFileHook(HANDLE aFileMappingObject,
169 : DWORD aDesiredAccess,
170 : DWORD aFileOffsetHigh,
171 : DWORD aFileOffsetLow,
172 : SIZE_T aNumBytesToMap)
173 : {
174 : LPVOID result = sMapViewOfFileOrig(aFileMappingObject, aDesiredAccess,
175 : aFileOffsetHigh, aFileOffsetLow,
176 : aNumBytesToMap);
177 : CheckMemAvailable();
178 : return result;
179 : }
180 :
181 : HBITMAP WINAPI
182 : CreateDIBSectionHook(HDC aDC,
183 : const BITMAPINFO* aBitmapInfo,
184 : UINT aUsage,
185 : VOID** aBits,
186 : HANDLE aSection,
187 : DWORD aOffset)
188 : {
189 : // There are a lot of calls to CreateDIBSection, so we make some effort not
190 : // to CheckMemAvailable() for calls to CreateDIBSection which allocate only
191 : // a small amount of memory.
192 :
193 : // If aSection is non-null, CreateDIBSection won't allocate any new memory.
194 : bool doCheck = false;
195 : if (sHooksActive && !aSection && aBitmapInfo) {
196 : uint16_t bitCount = aBitmapInfo->bmiHeader.biBitCount;
197 : if (bitCount == 0) {
198 : // MSDN says bitCount == 0 means that it figures out how many bits each
199 : // pixel gets by examining the corresponding JPEG or PNG data. We'll just
200 : // assume the worst.
201 : bitCount = 32;
202 : }
203 :
204 : // |size| contains the expected allocation size in *bits*. Height may be
205 : // negative (indicating the direction the DIB is drawn in), so we take the
206 : // absolute value.
207 : int64_t size = bitCount * aBitmapInfo->bmiHeader.biWidth *
208 : aBitmapInfo->bmiHeader.biHeight;
209 : if (size < 0) {
210 : size *= -1;
211 : }
212 :
213 : // If we're allocating more than 1MB, check how much memory is left after
214 : // the allocation.
215 : if (size > 1024 * 1024 * 8) {
216 : doCheck = true;
217 : }
218 : }
219 :
220 : HBITMAP result = sCreateDIBSectionOrig(aDC, aBitmapInfo, aUsage, aBits,
221 : aSection, aOffset);
222 :
223 : if (doCheck) {
224 : CheckMemAvailable();
225 : }
226 :
227 : return result;
228 : }
229 :
230 : static int64_t
231 : LowMemoryEventsVirtualDistinguishedAmount()
232 : {
233 : return sNumLowVirtualMemEvents;
234 : }
235 :
236 : static int64_t
237 : LowMemoryEventsPhysicalDistinguishedAmount()
238 : {
239 : return sNumLowPhysicalMemEvents;
240 : }
241 :
242 : class LowEventsReporter final : public nsIMemoryReporter
243 : {
244 : ~LowEventsReporter() {}
245 :
246 : public:
247 : NS_DECL_ISUPPORTS
248 :
249 : NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
250 : nsISupports* aData, bool aAnonymize) override
251 : {
252 : MOZ_COLLECT_REPORT(
253 : "low-memory-events/virtual", KIND_OTHER, UNITS_COUNT_CUMULATIVE,
254 : LowMemoryEventsVirtualDistinguishedAmount(),
255 : "Number of low-virtual-memory events fired since startup. We fire such an "
256 : "event if we notice there is less than memory.low_virtual_mem_threshold_mb of "
257 : "virtual address space available (if zero, this behavior is disabled). The "
258 : "process will probably crash if it runs out of virtual address space, so "
259 : "this event is dire.");
260 :
261 : MOZ_COLLECT_REPORT(
262 : "low-memory-events/commit-space", KIND_OTHER, UNITS_COUNT_CUMULATIVE,
263 : sNumLowCommitSpaceEvents,
264 : "Number of low-commit-space events fired since startup. We fire such an "
265 : "event if we notice there is less than memory.low_commit_space_threshold_mb of "
266 : "commit space available (if zero, this behavior is disabled). Windows will "
267 : "likely kill the process if it runs out of commit space, so this event is "
268 : "dire.");
269 :
270 : MOZ_COLLECT_REPORT(
271 : "low-memory-events/physical", KIND_OTHER, UNITS_COUNT_CUMULATIVE,
272 : LowMemoryEventsPhysicalDistinguishedAmount(),
273 : "Number of low-physical-memory events fired since startup. We fire such an "
274 : "event if we notice there is less than memory.low_physical_memory_threshold_mb "
275 : "of physical memory available (if zero, this behavior is disabled). The "
276 : "machine will start to page if it runs out of physical memory. This may "
277 : "cause it to run slowly, but it shouldn't cause it to crash.");
278 :
279 : return NS_OK;
280 : }
281 : };
282 : NS_IMPL_ISUPPORTS(LowEventsReporter, nsIMemoryReporter)
283 :
284 : #endif // defined(_M_IX86) && defined(XP_WIN)
285 :
286 : /**
287 : * This runnable is executed in response to a memory-pressure event; we spin
288 : * the event-loop when receiving the memory-pressure event in the hope that
289 : * other observers will synchronously free some memory that we'll be able to
290 : * purge here.
291 : */
292 0 : class nsJemallocFreeDirtyPagesRunnable final : public nsIRunnable
293 : {
294 0 : ~nsJemallocFreeDirtyPagesRunnable() {}
295 :
296 : public:
297 : NS_DECL_ISUPPORTS
298 : NS_DECL_NSIRUNNABLE
299 : };
300 :
301 0 : NS_IMPL_ISUPPORTS(nsJemallocFreeDirtyPagesRunnable, nsIRunnable)
302 :
303 : NS_IMETHODIMP
304 0 : nsJemallocFreeDirtyPagesRunnable::Run()
305 : {
306 0 : MOZ_ASSERT(NS_IsMainThread());
307 :
308 : #if defined(MOZ_MEMORY)
309 0 : jemalloc_free_dirty_pages();
310 : #endif
311 :
312 0 : return NS_OK;
313 : }
314 :
315 : /**
316 : * The memory pressure watcher is used for listening to memory-pressure events
317 : * and reacting upon them. We use one instance per process currently only for
318 : * cleaning up dirty unused pages held by jemalloc.
319 : */
320 3 : class nsMemoryPressureWatcher final : public nsIObserver
321 : {
322 0 : ~nsMemoryPressureWatcher() {}
323 :
324 : public:
325 : NS_DECL_ISUPPORTS
326 : NS_DECL_NSIOBSERVER
327 :
328 : void Init();
329 :
330 : private:
331 : static bool sFreeDirtyPages;
332 : };
333 :
334 9 : NS_IMPL_ISUPPORTS(nsMemoryPressureWatcher, nsIObserver)
335 :
336 : bool nsMemoryPressureWatcher::sFreeDirtyPages = false;
337 :
338 : /**
339 : * Initialize and subscribe to the memory-pressure events. We subscribe to the
340 : * observer service in this method and not in the constructor because we need
341 : * to hold a strong reference to 'this' before calling the observer service.
342 : */
343 : void
344 3 : nsMemoryPressureWatcher::Init()
345 : {
346 6 : nsCOMPtr<nsIObserverService> os = services::GetObserverService();
347 :
348 3 : if (os) {
349 3 : os->AddObserver(this, "memory-pressure", /* ownsWeak */ false);
350 : }
351 :
352 : Preferences::AddBoolVarCache(&sFreeDirtyPages, "memory.free_dirty_pages",
353 3 : false);
354 3 : }
355 :
356 : /**
357 : * Reacts to all types of memory-pressure events, launches a runnable to
358 : * free dirty pages held by jemalloc.
359 : */
360 : NS_IMETHODIMP
361 0 : nsMemoryPressureWatcher::Observe(nsISupports* aSubject, const char* aTopic,
362 : const char16_t* aData)
363 : {
364 0 : MOZ_ASSERT(!strcmp(aTopic, "memory-pressure"), "Unknown topic");
365 :
366 0 : if (sFreeDirtyPages) {
367 0 : nsCOMPtr<nsIRunnable> runnable = new nsJemallocFreeDirtyPagesRunnable();
368 :
369 0 : NS_DispatchToMainThread(runnable);
370 : }
371 :
372 0 : return NS_OK;
373 : }
374 :
375 : } // namespace
376 :
377 : namespace mozilla {
378 : namespace AvailableMemoryTracker {
379 :
380 : void
381 3 : Activate()
382 : {
383 : #if defined(_M_IX86) && defined(XP_WIN)
384 : MOZ_ASSERT(sInitialized);
385 : MOZ_ASSERT(!sHooksActive);
386 :
387 : Preferences::AddUintVarCache(&sLowVirtualMemoryThreshold,
388 : "memory.low_virtual_mem_threshold_mb", 256);
389 : Preferences::AddUintVarCache(&sLowPhysicalMemoryThreshold,
390 : "memory.low_physical_memory_threshold_mb", 0);
391 : Preferences::AddUintVarCache(&sLowCommitSpaceThreshold,
392 : "memory.low_commit_space_threshold_mb", 256);
393 : Preferences::AddUintVarCache(&sLowMemoryNotificationIntervalMS,
394 : "memory.low_memory_notification_interval_ms",
395 : 10000);
396 :
397 : RegisterStrongMemoryReporter(new LowEventsReporter());
398 : RegisterLowMemoryEventsVirtualDistinguishedAmount(
399 : LowMemoryEventsVirtualDistinguishedAmount);
400 : RegisterLowMemoryEventsPhysicalDistinguishedAmount(
401 : LowMemoryEventsPhysicalDistinguishedAmount);
402 : sHooksActive = true;
403 : #endif
404 :
405 : // This object is held alive by the observer service.
406 6 : RefPtr<nsMemoryPressureWatcher> watcher = new nsMemoryPressureWatcher();
407 3 : watcher->Init();
408 3 : }
409 :
410 : void
411 3 : Init()
412 : {
413 : // Do nothing on x86-64, because nsWindowsDllInterceptor is not thread-safe
414 : // on 64-bit. (On 32-bit, it's probably thread-safe.) Even if we run Init()
415 : // before any other of our threads are running, another process may have
416 : // started a remote thread which could call VirtualAlloc!
417 : //
418 : // Moreover, the benefit of this code is less clear when we're a 64-bit
419 : // process, because we aren't going to run out of virtual memory, and the
420 : // system is likely to have a fair bit of physical memory.
421 :
422 : #if defined(_M_IX86) && defined(XP_WIN)
423 : // Don't register the hooks if we're a build instrumented for PGO: If we're
424 : // an instrumented build, the compiler adds function calls all over the place
425 : // which may call VirtualAlloc; this makes it hard to prevent
426 : // VirtualAllocHook from reentering itself.
427 : if (!PR_GetEnv("MOZ_PGO_INSTRUMENTED")) {
428 : sKernel32Intercept.Init("Kernel32.dll");
429 : sKernel32Intercept.AddHook("VirtualAlloc",
430 : reinterpret_cast<intptr_t>(VirtualAllocHook),
431 : reinterpret_cast<void**>(&sVirtualAllocOrig));
432 : sKernel32Intercept.AddHook("MapViewOfFile",
433 : reinterpret_cast<intptr_t>(MapViewOfFileHook),
434 : reinterpret_cast<void**>(&sMapViewOfFileOrig));
435 :
436 : sGdi32Intercept.Init("Gdi32.dll");
437 : sGdi32Intercept.AddHook("CreateDIBSection",
438 : reinterpret_cast<intptr_t>(CreateDIBSectionHook),
439 : reinterpret_cast<void**>(&sCreateDIBSectionOrig));
440 : }
441 :
442 : sInitialized = true;
443 : #endif
444 3 : }
445 :
446 : } // namespace AvailableMemoryTracker
447 : } // namespace mozilla
|