Closed Bug 956501 Opened 11 years ago Closed 11 years ago

mozjemalloc can cause virtual address space fragmentation on linux

Categories

(Core :: Memory Allocator, defect)

x86
Linux
defect
Not set
normal

Tracking

()

RESOLVED FIXED
mozilla29

People

(Reporter: jonco, Assigned: jonco)

References

Details

(Whiteboard: [MemShrink])

Attachments

(1 file, 1 obsolete file)

TestTXMgr is failing like this in 32 bit linux opt builds with generational GC enabled: 09:08:19 INFO - ----------------------------------------------------- 09:08:19 INFO - - Aggregate Batch Transaction Stress Test: 09:08:19 INFO - ----------------------------------------------------- 09:08:24 INFO - Stress test of 500 iterations (may take a while) ... out of memory: 0x0000000000000200 bytes requested 09:08:24 INFO - cppunittests TEST-UNEXPECTED-FAIL | TestTXMgr | test failed with return code -11 I determined that this test takes 2.6 GB to run normally (without GGC), so I reckon that GGC is pushing it over the edge of exhausting the virtual address space.
This test should not run any JS code!
Yes, indeed. However something about building it in the GGC configuration is causing it to fail. GGC will allocate a nursery area of 16MB on startup, which I guessed was the source of the problem, even if it is unused by this test. I tried reducing the number of iterations for the failing test to 400 from 500. That way it passes locally, but still fails on try, which suggests it's not just going over some memory threshold. It passes in debug builds, even when changed to run the same number of test iterations as in opt builds. I ran it through valgrind and in debug builds it finds no problems, and in opt builds if crashes in the same way. So I'm pretty confused as to what's wrong here.
Do you have a stack trace for the crash? Is it an OOM?
It is an OOM - here's the backtrace for a debug build: #0 mozalloc_abort (msg=0xffffd246 "out of memory: 0x", '0' <repeats 13 times>, "200 bytes requested") at /home/jon/work/32bit/memory/mozalloc/mozalloc_abort.cpp:30 #1 0xf058ef53 in mozalloc_handle_oom (size=0) at /home/jon/work/32bit/memory/mozalloc/mozalloc_oom.cpp:50 #2 0xf058ee41 in moz_xmalloc (size=512) at /home/jon/work/32bit/memory/mozalloc/mozalloc.cpp:54 #3 0xf2b8f3f6 in operator new (size=<optimized out>, size=<optimized out>) at ../../../dist/include/mozilla/mozalloc.h:201 #4 __gnu_cxx::new_allocator<nsRefPtr<nsTransactionItem> >::allocate (this=0xffdff100, __n=128) at /usr/bin/../lib/gcc/i686-linux-gnu/4.7/../../../../include/c++/4.7/ext/new_allocator.h:94 #5 0xf2b8f38a in std::_Deque_base<nsRefPtr<nsTransactionItem>, std::allocator<nsRefPtr<nsTransactionItem> > >::_M_allocate_node (this=0xffdff100) at /usr/bin/../lib/gcc/i686-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_deque.h:534 #6 0xf2b9056f in std::_Deque_base<nsRefPtr<nsTransactionItem>, std::allocator<nsRefPtr<nsTransactionItem> > >::_M_create_nodes (this=0xffdff100, __nstart=0xffdfe6ec, __nfinish=0xffdfe6f0) at /usr/bin/../lib/gcc/i686-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_deque.h:628 #7 0xf2b90496 in std::_Deque_base<nsRefPtr<nsTransactionItem>, std::allocator<nsRefPtr<nsTransactionItem> > >::_M_initialize_map (this=0xffdff100, __num_elements=0) at /usr/bin/../lib/gcc/i686-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_deque.h:602 #8 0xf2b9039a in std::_Deque_base<nsRefPtr<nsTransactionItem>, std::allocator<nsRefPtr<nsTransactionItem> > >::_Deque_base (this=0xffdff100) at /usr/bin/../lib/gcc/i686-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_deque.h:454 #9 0xf2b90346 in std::deque<nsRefPtr<nsTransactionItem>, std::allocator<nsRefPtr<nsTransactionItem> > >::deque (this=0xffdff100) at /usr/bin/../lib/gcc/i686-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_deque.h:781 #10 0xf2b8e446 in std::deque<nsRefPtr<nsTransactionItem>, std::allocator<nsRefPtr<nsTransactionItem> > >::deque (this=0xffdff100) at /usr/bin/../lib/gcc/i686-linux-gnu/4.7/../../../../include/c++/4.7/bits/stl_deque.h:781 #11 0xf2b8d52f in nsTransactionStack::nsTransactionStack (this=0xffdff100, aType=nsTransactionStack::FOR_REDO) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionStack.cpp:16 #12 0xf2b8637c in nsTransactionItem::UndoChildren (this=0xffd58e00, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:209 #13 0xf2b861bc in nsTransactionItem::UndoTransaction (this=0xffd58e00, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:180 #14 0xf2b8650d in nsTransactionItem::UndoChildren (this=0xffd58de0, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:236 #15 0xf2b861bc in nsTransactionItem::UndoTransaction (this=0xffd58de0, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:180 #16 0xf2b8650d in nsTransactionItem::UndoChildren (this=0xffd58ba0, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:236 #17 0xf2b861bc in nsTransactionItem::UndoTransaction (this=0xffd58ba0, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:180 #18 0xf2b8650d in nsTransactionItem::UndoChildren (this=0xffd58b80, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:236 #19 0xf2b861bc in nsTransactionItem::UndoTransaction (this=0xffd58b80, aTxMgr=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionItem.cpp:180 #20 0xf2b89f3e in nsTransactionManager::UndoTransaction (this=0xed8cdd40) at /home/jon/work/32bit/editor/txmgr/src/nsTransactionManager.cpp:134 #21 0x080592cf in stress_test (factory=0xffffd8d0, iterations=500) at /home/jon/work/32bit/editor/txmgr/tests/TestTXMgr.cpp:4421 #22 0x0805981a in aggregation_batch_stress_test () at /home/jon/work/32bit/editor/txmgr/tests/TestTXMgr.cpp:4594 #23 0x08059e07 in main (argc=1, argv=0xffffda64) at /home/jon/work/32bit/editor/txmgr/tests/TestTXMgr.cpp:4636 The interesting thing is that when it crashes, the address space is alternately filled with 1MB chunks and unmapped memory: (gdb) info proc mappings process 25457 Mapped address spaces: Start Addr End Addr Size Offset objfile 0x100000 0x200000 0x100000 0x0 0x300000 0x400000 0x100000 0x0 0x500000 0x600000 0x100000 0x0 0x700000 0x800000 0x100000 0x0 0x900000 0xa00000 0x100000 0x0 0xb00000 0xc00000 0x100000 0x0 0xd00000 0xe00000 0x100000 0x0 0xf00000 0x1000000 0x100000 0x0 0x1100000 0x1200000 0x100000 0x0 0x1300000 0x1400000 0x100000 0x0 0x1500000 0x1600000 0x100000 0x0 0x1700000 0x1800000 0x100000 0x0 0x1900000 0x1a00000 0x100000 0x0 0x1b00000 0x1c00000 0x100000 0x0 0x1d00000 0x1e00000 0x100000 0x0 0x1f00000 0x2000000 0x100000 0x0 0x2100000 0x2200000 0x100000 0x0 0x2300000 0x2400000 0x100000 0x0 0x2500000 0x2600000 0x100000 0x0 ... pattern repeats ... 0xe8600000 0xe8700000 0x100000 0x0 0xe8800000 0xe8900000 0x100000 0x0 0xe8a00000 0xe8b00000 0x100000 0x0 0xe8c00000 0xe8d00000 0x100000 0x0 0xe8dfe000 0xe8dff000 0x1000 0x0 0xe8dff000 0xe95ff000 0x800000 0x0 [stack:25468] 0xe95ff000 0xe9600000 0x1000 0x0 0xe9600000 0xe9f00000 0x900000 0x0 [stack:25467] 0xea000000 0xea100000 0x100000 0x0 0xea200000 0xea300000 0x100000 0x0 0xea3ff000 0xea400000 0x1000 0x0 0xea400000 0xead00000 0x900000 0x0 [stack:25466] 0xeadff000 0xeae00000 0x1000 0x0 0xeae00000 0xec600000 0x1800000 0x0 [stack:25465] 0xec7fe000 0xec7ff000 0x1000 0x0 0xec7ff000 0xecfff000 0x800000 0x0 [stack:25464] 0xecfff000 0xed000000 0x1000 0x0 0xed000000 0xed900000 0x900000 0x0 [stack:25463] 0xed9bf000 0xed9c2000 0x3000 0x0 /usr/lib/i386-linux-gnu/gconv/UTF-16.so 0xed9c2000 0xed9c3000 0x1000 0x2000 /usr/lib/i386-linux-gnu/gconv/UTF-16.so 0xed9c3000 0xed9c4000 0x1000 0x3000 /usr/lib/i386-linux-gnu/gconv/UTF-16.so ... more code areas follow I've confirmed this doesn't happen on non-GGC builds.
Tracing jemalloc's calls to mmap, I found that memory is allocated in decreasing address order on my system (linux 3.8.0). jemalloc seems to assume the opposite. jemalloc tries to allocate aligned chunks of memory (where alignment > page size) making use of mmap, which doesn't allow alignment to be specified. This happens in chunk_alloc_mmap(), which also calls chunk_alloc_mmap_slow(). The alignment is called chunksize, which is 1MB. The algorithm is something like this: 1. Allocate a chunk of the size requested. If it's aligned, return it (this is the optimisitic fast path). 2. Try and make an aligned chunk by allocating sufficient extra memory at the end. If that succeeded, free the unused portion at the beginning and return. 3. Otherwise allocate size + alignment bytes Return the lowest aligned chunk and unmap the rest (this is the slow path). So step 2 will always fail in a system like this, as there will always be allocated address space after the returned mapping. Also, if mmap in step 3 returns an aligned block, the end of the block will be unmapped leaving an alignment-sized hole in the mappings. In this case we are repeatedly allocating 1MB chunks to extend the heap. I saw that the same block of free memory was returned every time in step 1, and it was not aligned. In the memory map above, it is 0xec6fe000, corresponding to the free space between these mappings: 0xeae00000 0xec600000 0x1800000 0x0 [stack:25465] 0xec7fe000 0xec7ff000 0x1000 0x0 Attempting to allocate after this fails, so the slow path is taken every time, successfully allocating a 2M area and then unmapping the higher 1MB, leading to this pattern of mappings and holes.
Component: Editor → jemalloc
Thanks for the fantastic analysis. I think Nick will be interested in this too, MemShrink wise.
Whiteboard: [MemShrink]
(In reply to Jon Coppeard (:jonco) from comment #2) > Yes, indeed. However something about building it in the GGC configuration > is causing it to fail. GGC will allocate a nursery area of 16MB on startup, > which I guessed was the source of the problem, even if it is unused by this > test. This is off topic, but that's going to be brutal on b2g.
(In reply to comment #7) > (In reply to Jon Coppeard (:jonco) from comment #2) > > Yes, indeed. However something about building it in the GGC configuration > > is causing it to fail. GGC will allocate a nursery area of 16MB on startup, > > which I guessed was the source of the problem, even if it is unused by this > > test. > > This is off topic, but that's going to be brutal on b2g. Bug 957723.
Summary: GenerationalGC: TestTXMgr test fails with out of memory in stress test on 32bit linux → mozjemalloc can cause virtual address space fragmentation on linux
Attached patch jemalloc-direction (obsolete) (deleted) — Splinter Review
Here's a possible fix. There are two parts to this: 1. In chunk_alloc_mmap(), if we initially allocate an unlaigned chunk, try to extend it down in memory as well as up. As noted above, if the OS is allocating address space in decreasing order then allocating up is never going to work. 2. In chunk_alloc_mmap_slow(), we don't need to allocate |size + chunksize| bytes to guarantee an aligned chunk, only |size + chunksize - pagesize|. This has the advantage that if the OS allocates addresses in decreasing order then it will not leave a hole. I found this by looking at the latest version of jemalloc. Green try run here: https://tbpl.mozilla.org/?tree=Try&rev=11c1b318ce1b It's possible that this is makes things too complicated. The latest version of jemalloc doesn't try to extend unaligned chunks in chunk_alloc_mmap(), it just falls back to the slow path straight away. This is another possible solution, but in the case where this behaviour arises it will result in us trying the fast path and then falling back to the slow path on every chunk allocation, as the fast path will receive the same unaligned chunk every time. I checked that with this patch the fast path succeeds on almost every allocation.
Attachment #8357810 - Flags: review?(mh+mozilla)
Comment on attachment 8357810 [details] [diff] [review] jemalloc-direction Review of attachment 8357810 [details] [diff] [review]: ----------------------------------------------------------------- I'd rather just import the newer code from jemalloc3, since it's what we're going to be using in the end anyways. Better be prepared to its mmap patterns (and who knows, maybe we'll find something wrong with it)
Attachment #8357810 - Flags: review?(mh+mozilla) → review-
Attached patch jemalloc-direction-v2 (deleted) — Splinter Review
Yes, that makes sense. Here's a patch to import as near as possible the latest code for chunk_alloc_mmap_slow() and chunk_alloc_mmap(). This is actually quite a lot simpler than what we've got at the moment. We try to allocate an aligned chunk and if this fails we just unmap it and try the slow path. There's no extension and no flag to try the slow path first next time. Try run here: https://tbpl.mozilla.org/?tree=Try&rev=20e38ef1e020
Attachment #8357810 - Attachment is obsolete: true
Attachment #8358805 - Flags: review?(mh+mozilla)
Comment on attachment 8358805 [details] [diff] [review] jemalloc-direction-v2 Review of attachment 8358805 [details] [diff] [review]: ----------------------------------------------------------------- This matches what is in jemalloc3. Thanks.
Attachment #8358805 - Flags: review?(mh+mozilla) → review+
Status: NEW → RESOLVED
Closed: 11 years ago
Resolution: --- → FIXED
Target Milestone: --- → mozilla29
Blocks: defrag
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: