Closed Bug 1903297 Opened 2 years ago Closed 1 year ago

WebRender spends a lot of time in jemalloc

Categories

(Core :: Graphics: WebRender, defect, P2)

defect

Tracking

()

RESOLVED FIXED

People

(Reporter: nical, Unassigned)

References

(Blocks 1 open bug)

Details

WebRender makes a lot of memory allocations. There is also a pattern of allocating memory on a thread and sending to another where it is eventually dropped. For example:

  • The scene is built on the scene builder thread, then sent to the render backend thread where it is dropped after being replaced by the net one
  • The BuiltFrame produced eahc frame on the render backend thread is sent to the renderer thread where it is dropped.
  • Blob images and glyph buffers are allocated on worker threads and dropped on the render backend thread or renderer thread.

The problems

Lock Contention

WebRender's threads each have their own jemalloc arena protected by a mutex. We are measuring a lot of contention on these locks.

Here is an example profile: https://share.firefox.dev/4elUEtu the lock contention is rather extreme in this particular case, but the web page itself is not doing something unreasonable or rare. 48% of the combined non-idle time of all webrender threads is spent waiting for a mutex in jemalloc. On other profiles it's typically somewhere between 8 and 15%. Here is another profile with 12% of the non-idle time spent in blocking on jemalloc locks https://share.firefox.dev/45oUFsA

Lock contention looks similar on when profiled on other OSes.

This lock contention affects all allocations webrender threads, not only the ones that were allocated in different threads. We expect that by more carefully allocating and deallocating memory on the same threads, we can greatly reduce the lock contention, and if we are strict about it, remove contention entirely. This is likely to be what will yield the biggest improvements in this bug.

Poisoning memory during deallocation

We memset the first 256 bytes of all memory blocks when deallocating them to poison memory. It shows up in profiles as well.

Syscalls when giving the memory back to the OS.

For example madvise on linux. This shows up pretty high in profiles as well.

Jemalloc starts giving memory back to the OS after a threshold which by default is 256 allocated pages (per arena). The threshold can be tweaked via moz_set_max_dirty_page_modifier. The MALLOC_OPTIONS environment variable (only used in debug builds) can be used to tweak the threshold (add a number of lower case "f" to decrease the threshold and upper case "F" to increase it).

For this the situation may depend on the platform:

  • On Linux we call madvise during deallocation if we passed the threshold and a page is ready to be deallocated
  • On Mac (My undertanding of this is fuzzy), we keep the deallocated page around a bit longer and hand them back to the OS in batches as part of a cleanup phase, kicked by the cycle collector. Or at least that's how it goes in content processes, I'm not sure whether it is the case on the parent process.
  • Other platforms: TODO

Solutions

Send back allocations

For malloc contention, we need to send large groups of allocations (for example scenes, frames, blob and glyph buffers) back to the threads that create them. This is uncontroversial and has a potentially large impact.

Better recycle allocations in general

Typically, some allocations can be kept around and reused the next frame. We've done several passes of that over the years, it generally provides easy but modest wins. There aren't a lot of low hanging fruits left but there may be some still.

Specialized allocators

We could place allocations for large data structures that move betwen threads into specialized allocators, for example bump allocators would work very well for scenes and frames. These would call into jemalloc only to allocate the large backing buffers of these specialized allocators. Each frame for instance, would have its own allocator which would be sent back to the render backend thread for reuse once the frame is dropped.

The benefits would include:

  • Allocation and deallocation is very fast,
  • Grouping allocations means much fewer system calls and memory poisoning,
  • We control when to give memory back to the OS per specialized allocator instead of at the thread level,
  • We can more easily track memory usage per system (for example the scene, the frame, etc).

Disadvantages/risks:

  • Since memory is handed back to the OS based on a threshold, if we move and keep large amounts of allocated pages in our own areanas, there is a risk that it will simply put pressure into other memory allocations and move the cost somewhere else on the same thread.
  • We can't use standard data structure (Box, Vec, HashMap, etc.) with custom allocators, so we have to important duplicates of them that support the allocator API.
  • There is a bug with allocator_api's Box implementation which can't happen for valid uses of the standard Box, but fails to catch the invalid use at compile time. (TODO: link)

Tweak moz_set_max_dirty_page_modifier on some threads

Perhaps some simple adjustments here can have an impact on common workloads. If we are worried about increasing the number of pages that webrender threads keep around, we can remove some thread in WebRender, for example cap the maximum number of worker threads and merge all render backend threads into a single one.

Disable memory poisoning on some threads

This could help with mitigating the time spent poisoning memory if we don't chose to integrate specialized allocators.

WebRender threads run almost exclusively rust code. The main exceptions are skia and freetype/coregraphics/dwrite on worker threads and the graphics driver on the renderer thread. Graphics drivers use their own memory allocators. There is no JS running in WebRender's threads and when the GPU process is enabled, no JS at all in the process.

Maybe in some situations we can afford to not poison memory on some or all of WebRender's threads.

Miscellaneous

  • Beware when measuring performance that debug builds (even optimized ones) also memset allocations which release builds don't.
  • jemalloc has per-size arenas for small sizes (every multiple of 16 bytes up to 512, then multiple of 256 bytes between 512 and 4kb. The fewer of these type categories we use the less fragmentation we have. Leveraging that could help with reducing memory usage and avoid hitting the threshold for handing pages back to the OS as often.
Blocks: 1587475
Depends on: 1903977
Depends on: 1903979
Depends on: 1903981

The jemalloc situation in WR improved dramatically since we switched most allocations during frame building to a bump allocator.

Status: NEW → RESOLVED
Closed: 1 year ago
Resolution: --- → FIXED
You need to log in before you can comment on or make changes to this bug.