From 45a219844fa76815a62c74ca8e4a83e98aad0b4e Mon Sep 17 00:00:00 2001 From: Marcin Mielczarczyk Date: Fri, 6 Mar 2026 15:56:06 +0100 Subject: [PATCH] ARRISEOS-48505: Implement heap breakdown for WPE and bmalloc Put IsoHeaps and other allocation categories into separate malloc heaps for profiling. --- Source/WTF/wtf/CMakeLists.txt | 16 ++ Source/WTF/wtf/DebugHeap.h | 4 - Source/WTF/wtf/MallocHeapBreakdown.cmake | 14 + Source/WTF/wtf/malloc_heap_breakdown/main.cpp | 260 ++++++++++++++++++ .../wtf/malloc_heap_breakdown/malloc/malloc.h | 76 +++++ Source/bmalloc/CMakeLists.txt | 6 + Source/bmalloc/bmalloc/BPlatform.h | 2 + Source/bmalloc/bmalloc/IsoMallocFallback.cpp | 4 +- Source/cmake/OptionsWPE.cmake | 4 + Source/cmake/WebKitFeatures.cmake | 1 + 10 files changed, 381 insertions(+), 6 deletions(-) create mode 100644 Source/WTF/wtf/MallocHeapBreakdown.cmake create mode 100644 Source/WTF/wtf/malloc_heap_breakdown/main.cpp create mode 100644 Source/WTF/wtf/malloc_heap_breakdown/malloc/malloc.h diff --git a/Source/WTF/wtf/CMakeLists.txt b/Source/WTF/wtf/CMakeLists.txt index c17dad079736a..6a139d6065bcc 100644 --- a/Source/WTF/wtf/CMakeLists.txt +++ b/Source/WTF/wtf/CMakeLists.txt @@ -1,3 +1,7 @@ +if (ENABLE_MALLOC_HEAP_BREAKDOWN AND NOT APPLE) + include(MallocHeapBreakdown.cmake) +endif () + set(WTF_PUBLIC_HEADERS ASCIICType.h AggregateLogger.h @@ -570,6 +574,10 @@ set(WTF_PRIVATE_INCLUDE_DIRECTORIES "${WTF_DIR}/wtf/unicode" ) +if (ENABLE_MALLOC_HEAP_BREAKDOWN AND NOT APPLE) + list(APPEND WTF_PRIVATE_INCLUDE_DIRECTORIES "${WTF_DIR}/wtf/malloc_heap_breakdown") +endif () + set(WTF_LIBRARIES ${CMAKE_DL_LIBS} ICU::data @@ -580,6 +588,10 @@ set(WTF_LIBRARIES gpumem ) +if (ENABLE_MALLOC_HEAP_BREAKDOWN AND NOT APPLE) + list(APPEND WTF_LIBRARIES MallocHeapBreakdown) +endif () + if (NOT USE_SYSTEM_MALLOC) set(WTF_FRAMEWORKS bmalloc) endif () @@ -598,6 +610,10 @@ set(WTF_INTERFACE_LIBRARIES WTF) set(WTF_INTERFACE_INCLUDE_DIRECTORIES ${WTF_FRAMEWORK_HEADERS_DIR}) set(WTF_INTERFACE_DEPENDENCIES WTF_CopyHeaders) +if (ENABLE_MALLOC_HEAP_BREAKDOWN AND NOT APPLE) + list(APPEND WTF_INTERFACE_INCLUDE_DIRECTORIES "${WTF_DIR}/wtf/malloc_heap_breakdown") +endif () + WEBKIT_FRAMEWORK_DECLARE(WTF) WEBKIT_INCLUDE_CONFIG_FILES_IF_EXISTS() diff --git a/Source/WTF/wtf/DebugHeap.h b/Source/WTF/wtf/DebugHeap.h index 562a36788b094..4525c1c23c79e 100644 --- a/Source/WTF/wtf/DebugHeap.h +++ b/Source/WTF/wtf/DebugHeap.h @@ -30,10 +30,8 @@ #if ENABLE(MALLOC_HEAP_BREAKDOWN) #include -#if OS(DARWIN) #include #endif -#endif namespace WTF { @@ -50,9 +48,7 @@ class DebugHeap { WTF_EXPORT_PRIVATE void free(void*); private: -#if OS(DARWIN) malloc_zone_t* m_zone; -#endif }; #define DECLARE_ALLOCATOR_WITH_HEAP_IDENTIFIER(Type) \ diff --git a/Source/WTF/wtf/MallocHeapBreakdown.cmake b/Source/WTF/wtf/MallocHeapBreakdown.cmake new file mode 100644 index 0000000000000..6b20ac85d754a --- /dev/null +++ b/Source/WTF/wtf/MallocHeapBreakdown.cmake @@ -0,0 +1,14 @@ +add_library(MallocHeapBreakdown STATIC + malloc_heap_breakdown/main.cpp +) +add_library(WebKit::MallocHeapBreakdown ALIAS MallocHeapBreakdown) + +target_include_directories(MallocHeapBreakdown + PUBLIC + malloc_heap_breakdown +) + +if (USE_SYSPROF_CAPTURE) + target_link_libraries(MallocHeapBreakdown PRIVATE SysProfCapture::SysProfCapture) + target_compile_definitions(MallocHeapBreakdown PRIVATE USE_SYSPROF_CAPTURE) +endif () diff --git a/Source/WTF/wtf/malloc_heap_breakdown/main.cpp b/Source/WTF/wtf/malloc_heap_breakdown/main.cpp new file mode 100644 index 0000000000000..300a751148735 --- /dev/null +++ b/Source/WTF/wtf/malloc_heap_breakdown/main.cpp @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2025 Igalia S.L. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY IGALIA S.L. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "malloc/malloc.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if USE_SYSPROF_CAPTURE +#include +#endif + +static std::string currentExecutablePath() +{ + static char buffer[4096]; + ssize_t result = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1); + if (result == -1) + return ""; + buffer[result] = '\0'; + return buffer; +} + +class MallocZoneHeapManager { +public: + static MallocZoneHeapManager& getInstance() + { + static MallocZoneHeapManager singleton; + return singleton; + } + + malloc_zone_t* defaultZone() { return &m_defaultZone; } + + malloc_zone_t* createZone() + { + const std::lock_guard lock(m_mutex); + auto zone = std::make_unique(); + auto* zonePtr = zone.get(); + m_zoneAllocations.emplace(zonePtr, std::map()); + m_zoneNames.emplace(zonePtr, "No name"); + m_zoneObjects.insert(std::move(zone)); + return zonePtr; + } + + void renameZone(malloc_zone_t* zone, const char* name) + { + const std::lock_guard lock(m_mutex); + if (!zone || !m_zoneNames.count(zone)) + return; + m_zoneNames[zone] = name ? name : "No name"; + } + + void* zoneMalloc(malloc_zone_t* zone, size_t size) + { + const std::lock_guard lock(m_mutex); + if (!zone || !m_zoneNames.count(zone)) + return nullptr; + void* memory = malloc(size); + if (memory) + m_zoneAllocations[zone].emplace(memory, size); + return memory; + } + + void* zoneCalloc(malloc_zone_t* zone, size_t numItems, size_t size) + { + const std::lock_guard lock(m_mutex); + if (!zone || !m_zoneNames.count(zone)) + return nullptr; + void* memory = calloc(numItems, size); + if (memory) + m_zoneAllocations[zone].emplace(memory, numItems * size); + return memory; + } + + void* zoneRealloc(malloc_zone_t* zone, void* memory, size_t size) + { + const std::lock_guard lock(m_mutex); + if (!zone || !m_zoneNames.count(zone)) + return nullptr; + if (!memory) + return zoneMalloc(zone, size); + if (!size) { + zoneFree(zone, memory); + return nullptr; + } + void* ptr = realloc(memory, size); + if (ptr) { + if (ptr != memory) + m_zoneAllocations[zone].erase(memory); + m_zoneAllocations[zone][ptr] = size; + } + return ptr; + } + + void* zoneMemalign(malloc_zone_t* zone, size_t alignment, size_t size) + { + const std::lock_guard lock(m_mutex); + if (!zone || !m_zoneNames.count(zone)) + return nullptr; + void* memory = aligned_alloc(alignment, size); + if (memory) + m_zoneAllocations[zone].emplace(memory, size); + return memory; + } + + void zoneFree(malloc_zone_t* zone, void* memory) + { + const std::lock_guard lock(m_mutex); + if (!zone || !m_zoneNames.count(zone)) + return; + auto& allocations = m_zoneAllocations[zone]; + auto it = allocations.find(memory); + if (it == allocations.end()) + return; + allocations.erase(it); + free(memory); + } + +private: + MallocZoneHeapManager() + { + const char* intervalEnv = getenv("WEBKIT_MALLOC_HEAP_BREAKDOWN_LOG_INTERVAL"); + int intervalSeconds = intervalEnv ? atoi(intervalEnv) : 3; + m_monitorInterval = std::chrono::seconds(intervalSeconds); + + std::printf("MallocZoneHeapManager created for PID:%d(%s)\n", getpid(), currentExecutablePath().c_str()); + + m_zoneAllocations.emplace(&m_defaultZone, std::map()); + m_zoneNames.emplace(&m_defaultZone, "Default Zone"); + + if (m_monitorInterval > std::chrono::seconds(0)) + m_monitorThread = std::thread(&MallocZoneHeapManager::monitoringThreadMain, this); + } + + ~MallocZoneHeapManager() + { + m_forceThreadExit = true; + if (m_monitorThread.has_value()) + m_monitorThread->join(); + } + + void printHeapBreakdown() + { + std::printf("%d Malloc Heap Breakdown: | PID | \"Zone name\" | Number of allocated chunks | Total bytes allocated | {\n", getpid()); + size_t grandTotal = 0; + for (const auto& [zonePtr, zoneName] : m_zoneNames) { + size_t totalBytes = 0; + for (const auto& [memPtr, bytes] : m_zoneAllocations[zonePtr]) + totalBytes += bytes; + grandTotal += totalBytes; + std::printf("%d \"%s\" %zu %zu\n", getpid(), zoneName.c_str(), m_zoneAllocations[zonePtr].size(), totalBytes); + } + std::printf("%d } Malloc Heap Breakdown: grand total bytes allocated: %zu\n", getpid(), grandTotal); + } + + void monitoringThreadMain() + { + // Stagger startup to avoid all processes logging at the same time. + srand(static_cast(time(nullptr))); + std::this_thread::sleep_for(std::chrono::milliseconds(rand() % 3000)); + + while (!m_forceThreadExit) { + { + const std::lock_guard lock(m_mutex); + printHeapBreakdown(); + } + std::this_thread::sleep_for(m_monitorInterval); + } + } + + std::chrono::seconds m_monitorInterval { 3 }; + malloc_zone_t m_defaultZone; + std::map> m_zoneAllocations; + std::map m_zoneNames; + std::set> m_zoneObjects; + std::recursive_mutex m_mutex; + std::atomic m_forceThreadExit { false }; + std::optional m_monitorThread; +}; + +malloc_zone_t* malloc_default_zone() +{ + return MallocZoneHeapManager::getInstance().defaultZone(); +} + +malloc_zone_t* malloc_create_zone(vm_size_t, unsigned) +{ + return MallocZoneHeapManager::getInstance().createZone(); +} + +void* malloc_zone_malloc(malloc_zone_t* zone, size_t size) +{ + return MallocZoneHeapManager::getInstance().zoneMalloc(zone, size); +} + +void* malloc_zone_calloc(malloc_zone_t* zone, size_t num_items, size_t size) +{ + return MallocZoneHeapManager::getInstance().zoneCalloc(zone, num_items, size); +} + +void malloc_zone_free(malloc_zone_t* zone, void* ptr) +{ + MallocZoneHeapManager::getInstance().zoneFree(zone, ptr); +} + +void* malloc_zone_realloc(malloc_zone_t* zone, void* ptr, size_t size) +{ + return MallocZoneHeapManager::getInstance().zoneRealloc(zone, ptr, size); +} + +void* malloc_zone_memalign(malloc_zone_t* zone, size_t alignment, size_t size) +{ + return MallocZoneHeapManager::getInstance().zoneMemalign(zone, alignment, size); +} + +void malloc_set_zone_name(malloc_zone_t* zone, const char* name) +{ + MallocZoneHeapManager::getInstance().renameZone(zone, name); +} + +size_t malloc_zone_pressure_relief(malloc_zone_t*, size_t) +{ + return 0; +} + +void malloc_zone_print(malloc_zone_t*, boolean_t) +{ +} diff --git a/Source/WTF/wtf/malloc_heap_breakdown/malloc/malloc.h b/Source/WTF/wtf/malloc_heap_breakdown/malloc/malloc.h new file mode 100644 index 0000000000000..9904dc955cb7c --- /dev/null +++ b/Source/WTF/wtf/malloc_heap_breakdown/malloc/malloc.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2025 Igalia S.L. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY IGALIA S.L. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +/* This file was inspired by malloc.h from MacOS. */ + +typedef struct _malloc_zone_t { +} malloc_zone_t; + +typedef size_t vm_size_t; +typedef int boolean_t; + +/********* Creation and destruction ************/ + +extern malloc_zone_t* malloc_default_zone(void); + /* The initial zone */ + +extern malloc_zone_t* malloc_create_zone(vm_size_t start_size, unsigned flags); + /* Creates a new zone with default behavior and registers it */ + +/********* Block creation and manipulation ************/ + +extern void* malloc_zone_malloc(malloc_zone_t* zone, size_t size); + /* Allocates a new pointer of size size; zone must be non-NULL */ + +extern void* malloc_zone_calloc(malloc_zone_t* zone, size_t num_items, size_t size); + /* Allocates a new pointer of size num_items * size; block is cleared; zone must be non-NULL */ + +extern void malloc_zone_free(malloc_zone_t* zone, void* ptr); + /* Frees pointer in zone; zone must be non-NULL */ + +extern void* malloc_zone_realloc(malloc_zone_t* zone, void* ptr, size_t size); + /* Enlarges block if necessary; zone must be non-NULL */ + +extern void* malloc_zone_memalign(malloc_zone_t* zone, size_t alignment, size_t size); + /* + * Allocates a new pointer of size size whose address is an exact multiple of alignment. + * alignment must be a power of two and at least as large as sizeof(void *). + * zone must be non-NULL. + */ + +/********* Functions for zone implementors ************/ + +extern void malloc_set_zone_name(malloc_zone_t* zone, const char* name); + /* Sets the name of a zone */ + +size_t malloc_zone_pressure_relief(malloc_zone_t* zone, size_t goal); + +/********* Debug helpers ************/ + +extern void malloc_zone_print(malloc_zone_t* zone, boolean_t verbose); diff --git a/Source/bmalloc/CMakeLists.txt b/Source/bmalloc/CMakeLists.txt index ae63639659f75..4f667202b4f59 100644 --- a/Source/bmalloc/CMakeLists.txt +++ b/Source/bmalloc/CMakeLists.txt @@ -659,6 +659,12 @@ if (CMAKE_SYSTEM_NAME MATCHES "Darwin") ) endif () +if (ENABLE_MALLOC_HEAP_BREAKDOWN) + list(APPEND bmalloc_PRIVATE_INCLUDE_DIRECTORIES "${CMAKE_SOURCE_DIR}/Source/WTF/wtf/malloc_heap_breakdown") + list(APPEND bmalloc_DEFINITIONS BENABLE_MALLOC_HEAP_BREAKDOWN=1) + list(APPEND bmalloc_LIBRARIES ${MALLOC_HEAP_BREAKDOWN_LIBRARIES}) +endif () + set(bmalloc_LIBRARIES ${CMAKE_DL_LIBS} ) diff --git a/Source/bmalloc/bmalloc/BPlatform.h b/Source/bmalloc/bmalloc/BPlatform.h index 0eae025bc8253..f953616b8cbc5 100644 --- a/Source/bmalloc/bmalloc/BPlatform.h +++ b/Source/bmalloc/bmalloc/BPlatform.h @@ -317,7 +317,9 @@ #endif /* Enable this to put each IsoHeap and other allocation categories into their own malloc heaps, so that tools like vmmap can show how big each heap is. */ +#if !defined(BENABLE_MALLOC_HEAP_BREAKDOWN) #define BENABLE_MALLOC_HEAP_BREAKDOWN 0 +#endif /* This is used for debugging when hacking on how bmalloc calculates its physical footprint. */ #define ENABLE_PHYSICAL_PAGE_MAP 0 diff --git a/Source/bmalloc/bmalloc/IsoMallocFallback.cpp b/Source/bmalloc/bmalloc/IsoMallocFallback.cpp index 902b6a398f585..a54c15cf947fa 100644 --- a/Source/bmalloc/bmalloc/IsoMallocFallback.cpp +++ b/Source/bmalloc/bmalloc/IsoMallocFallback.cpp @@ -62,7 +62,7 @@ void determineMallocFallbackState() MallocResult tryMalloc( size_t size #if BENABLE_MALLOC_HEAP_BREAKDOWN - , malloc_zone_t* zone = nullptr + , malloc_zone_t* zone #endif ) { @@ -87,7 +87,7 @@ MallocResult tryMalloc( bool tryFree( void* ptr #if BENABLE_MALLOC_HEAP_BREAKDOWN - , malloc_zone_t* zone = nullptr + , malloc_zone_t* zone #endif ) { diff --git a/Source/cmake/OptionsWPE.cmake b/Source/cmake/OptionsWPE.cmake index 09e563cbba1c2..4679906eeeacc 100644 --- a/Source/cmake/OptionsWPE.cmake +++ b/Source/cmake/OptionsWPE.cmake @@ -129,6 +129,10 @@ else () WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_RESOURCE_USAGE PRIVATE OFF) endif () +if (ENABLE_MALLOC_HEAP_BREAKDOWN) + set(MALLOC_HEAP_BREAKDOWN_LIBRARIES MallocHeapBreakdown) +endif () + if (ENABLE_DEVELOPER_MODE) WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_API_TESTS PRIVATE ON) WEBKIT_OPTION_DEFAULT_PORT_VALUE(ENABLE_MINIBROWSER PUBLIC ON) diff --git a/Source/cmake/WebKitFeatures.cmake b/Source/cmake/WebKitFeatures.cmake index 7e0d96890303d..511f5b16836db 100644 --- a/Source/cmake/WebKitFeatures.cmake +++ b/Source/cmake/WebKitFeatures.cmake @@ -202,6 +202,7 @@ macro(WEBKIT_OPTION_BEGIN) WEBKIT_OPTION_DEFINE(ENABLE_MEDIA_SOURCE "Toggle Media Source support" PRIVATE OFF) WEBKIT_OPTION_DEFINE(ENABLE_MEDIA_STATISTICS "Toggle Media Statistics support" PRIVATE OFF) WEBKIT_OPTION_DEFINE(ENABLE_MEDIA_STREAM "Toggle Media Stream support" PRIVATE OFF) + WEBKIT_OPTION_DEFINE(ENABLE_MALLOC_HEAP_BREAKDOWN "Put IsoHeaps and other allocation categories into separate malloc heaps for profiling" PRIVATE OFF) WEBKIT_OPTION_DEFINE(ENABLE_MEMORY_SAMPLER "Toggle Memory Sampler support" PRIVATE OFF) WEBKIT_OPTION_DEFINE(ENABLE_MHTML "Toggle MHTML support" PRIVATE OFF) WEBKIT_OPTION_DEFINE(ENABLE_MINIBROWSER "Toggle MiniBrowser compilation." PRIVATE OFF)