{"id":25778581,"url":"https://github.com/tier4/heaphook","last_synced_at":"2025-02-27T06:36:03.257Z","repository":{"id":98863376,"uuid":"596806493","full_name":"tier4/heaphook","owner":"tier4","description":"Replace all the dynamic heap allocation functions by LD_PRELOAD.","archived":false,"fork":false,"pushed_at":"2024-06-04T03:44:57.000Z","size":54,"stargazers_count":23,"open_issues_count":2,"forks_count":5,"subscribers_count":49,"default_branch":"main","last_synced_at":"2025-02-23T12:46:46.473Z","etag":null,"topics":["ldpreload","malloc","ros2"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tier4.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.rst","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-03T00:47:27.000Z","updated_at":"2025-02-11T03:34:21.000Z","dependencies_parsed_at":"2024-06-04T04:45:35.916Z","dependency_job_id":null,"html_url":"https://github.com/tier4/heaphook","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tier4%2Fheaphook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tier4%2Fheaphook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tier4%2Fheaphook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tier4%2Fheaphook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tier4","download_url":"https://codeload.github.com/tier4/heaphook/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240990427,"owners_count":19889871,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ldpreload","malloc","ros2"],"created_at":"2025-02-27T06:36:02.719Z","updated_at":"2025-02-27T06:36:03.243Z","avatar_url":"https://github.com/tier4.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# heaphook\nReplace all the dynamic heap allocation functions by LD_PRELOAD.\n\n## Build and Install\nThis library `heaphook` is prepared as a `ament_cmake` package.\n```\n$ mkdir -p /path/to/heaphook_ws \u0026\u0026 cd /path/to/heaphook_ws\n$ mkdir src \u0026 git clone git@github.com:tier4/heaphook.git src\n$ colcon build\n```\nThe shared libraries that will be specified in `LD_PRELOAD` are generated under `install/heaphook/lib`.\nThis path is added to `LD_LIBRARY_PATH` by the setup script.\n```\n$ source install/setup.bash\n```\n\n## How to use\nFor now, We provide two kinds of the heaphook libraries.\n- `libpreloaded_heaptrack.so`: Records all the heap allocation/deallocation function calls and generate a log file for visualizing the history of heap consumption.\n- `libpreloaded_tlsf.so`: Replaces all the heap allocation/deallocation with TLSF (Tow-Level Segregated Fit) memory allocator.\n- `libpreloaded_backtrace.so`: Records all malloc/new function calls with their backtraces where the memory allocations take place.\n\nA typical use case is to utilize `libpreloaded_heaptrack` to grasp the transition and maximum value of heap consumtion of the target process\nand to determine the initial allocated memory pool size for `libpreloaded_tlsf`.\nOf cource, it is also useful to utilize `libpreloaded_heaptrack` just to know the heap consumption of the target process.\n\n### libpreloaded_heaptrack\nYou can track the heap consumption of the specified process.\n```\n$ LD_PRELOAD=libpreloaded_heaptrack.so executable\n```\nA log file is generated under the current working directory in the format `heaplog.{%pid}.log`.\nYou can visualize heap consumption transitions in PDF format based on the generated log file.\nThe parser depends on `progressbar` python library, so install it before.\n```\n$ pip install progressbar\n$ python3 heaplog_parser.py heaplog.{%pid}.log // Generates heaplog.{%pid}.pdf\n```\n\n\n### libpreloaded_tlsf\nYou need to specify the initial allocaton size for the memory pool and the size of additional memory to be allocated\nwhen the initial memory pool size is not sufficient.\nThe initial size of the memory pool should be set to a value with a margin greater than the peak heap usage of the target process.\nExtending the memory pool size during the runtime should be avoided, as it leads to overhead in the allocation functions.\n```\n$ LD_PRELOAD=libpreloaded_tlsf.so INITIAL_MEMPOOL_SIZE=1000000 ADDITIONAL_MEMPOOL_SIZE=1000000 executable\n```\n\nWhen the initial size of the memory pool is insufficient, it first allocates the additional size specified by `ADDITIONAL_MEMPOOL_SIZE`,\nand if it is still insufficient, it allocates double the value of the previous allocation until it is sufficient.\n\nAs an example, here is what happens when `malloc(1000)` is called when the existing memory pool is exhausted and `ADDITIONAL_MEMPOOL_SIZE=100`.\n1. 100 bytes added to the memory pool (still not sufficient)\n2. 200 bytes added to the memory pool (still not sufficient)\n3. 400 bytes added to the memory pool (still not sufficient)\n4. 800 bytes added to the memory pool (still not sufficient)\n5. 1600 bytes added to the memory pool (finally sufficient)\n\nThe added memory pool areas are not contiguous with each other in the virual address space,\nso it is not necessarily enough even if the total size of the added memory pools exceeds the size of the memory allocation request. \n\n### libpreloaded_backtrace.so\nUse the following command to trace the callers:\n```\n$ LD_PRELOAD=libpreloaded_backtrace.so executable\n```\nTwo log files are genearted under the working directory in the format of `top_alloc_bytes_bt.{%pid}.{%tid}.log` and `top_num_calls_bt.{%pid}.{%tid}.log`. By default, top 10 callers are logged. The environment variable `NUM_TOPS` can control the number of callers. In addition, callers that just make one malloc/new are not logged as they are not the major source of page faults. To disable it, just set environment variable `SHOW_NON_RECURRENT_CALLERS=1`.\n\nTo show the source code file name and the line numbers, run the following command:\n```\n$ python3 backtrace_analyzer.py -i top_alloc_bytes_bt.{%pid}.{%tid}.log\nor\n$ python3 backtrace_analyzer.py -i top_alloc_bytes_bt.{%pid}.{%tid}.log | c++filt  // demangle C++ function names\n```\n\nThe result will be more user-friendly if the executables are linked with `-rdynamic -no-pie -fno-pie` options. Or even aggressive, build your code with `CMAKE_BUILD_TYPE=RelWithDebInfo`.\n\n## Integrate with ROS2 launch\nYou can easily integrate `heaphook` with ROS2 launch systems.\nFrom the launch file, you can replace all heap allocations of the process corresponding to the targeted `Node` and `ComposableNodeContainer`.\n\n```xml\n\u003cnode pkg=\"...\" exec=\"...\" name=\"...\"\u003e\n  \u003cenv name=\"LD_PRELOAD\" value=\"libpreloaded_heaptrack.so\" /\u003e\n\u003c/node\u003e\n```\n\n```xml\n\u003cnode pkg=\"...\" exec=\"...\" name=\"...\"\u003e\n  \u003cenv name=\"LD_PRELOAD\" value=\"libpreloaded_tlsf.so\" /\u003e\n  \u003cenv name=\"INITIAL_MEMPOOL_SIZE\" value=\"100000000\" /\u003e\n  \u003cenv name=\"ADDITIONAL_MEMPOOL_SIZE\" value=\"100000000\" /\u003e\n\u003c/node\u003e\n```\n\n```python\ncontainer = ComposableNodeContainer(\n  ...,\n  additional_env={\"LD_PRELOAD\": \"libpreloaded_heaptrack.so\"},\n)\n```\n\n```python\ncontainer = ComposableNodeContainer(\n  ...,\n  additional_env={\n    \"LD_PRELOAD\": \"libpreloaded_tlsf.so\",\n    \"INITIAL_MEMPOOL_SIZE\": \"100000000\", # 100MB\n    \"ADDITIONAL_MEMPOOL_SIZE\": \"100000000\",\n  },\n)\n```\n\n## How to add a new memory allocator\nIf you want to implement an allocator to replace the GLIBC memory allcator, you can easily do so by using this heaphook library.\n\nThe steps you will take are as follows.\n### 1. Create source file\nWhat you have to implement are\n* Include `heaphook/heaphook.hpp` header file.\n* Implement your own allocator class that inherits the abstract base class `GlobalAllocator` defined in `heaphook/heaphook.hpp`.\n  * This base class has 5 virtual functions: `do_alloc`, `do_dealloc`, `do_alloc_zeroed`, `do_realloc` and `do_get_block_size`.\n  * `do_alloc_zeroed` and `do_realloc` has default implementation, so you don't have to implement them.\n  * For more information on the GlobalAllocagor API, see here.\n* Implement static member function named `get_instance` in `GlobalAllocator`.\n  * The implementation of this static member function is almost a fixed form. It defines its own allocator as a static local variable and returns a reference to its instance.\n  * See the following example for a concrete implementation.\n\n\nThe minimum implementation required is as follows.\n```cpp\n#include \"heaphook/heaphook.hpp\"\nusing namespace heaphook;\n\nclass MyAllocator : public GlobalAllocator {\n  void* do_alloc(size_t size, size_t align) override {\n    ...\n  }\n\n  void do_dealloc(void* ptr) override {\n    ...\n  }\n\n  size_t do_get_block_size(void * ptr) override {\n    ...\n  }\n};\n\nGlobalAllocator \u0026GlobalAllocator::get_instance() {\n  static MyAllocator allocator;\n  return allocator;\n}\n```\nSave the above implementation in src/my_allocator.cpp.\n\n### 2. Edit CMakeLists.txt\nTo build the implemented allocator, you must edit CMakeLists.txt.\nYou can use build_library cmake function to add build target.\n\nbuild_library takes a library name as its first argument and a set of required source files as the second and subsequent arguments.\n\nFor example, \n```cmake\nbuild_library(my_allocator\n  src/my_allocator.cpp)\n```\n\n### 3. Build\nGo to the top directory and execute the following command.\n```shell\n$ colcon build\n```\nIf the build is successful, a file named `lib\u003clibname\u003e.so` should be created.\nYou can use it as follows.\n```shell\n$ source install/setup.bash\n$ LD_PRELOAD=libmy_allocator.so executable\n```\n\n## heaphook API\nThis section describes the `GlobalAllocator` class, which is required when creating an allocator.\n\nThe `GlobalAllocator` class has 5 virtual member functions.\n### do_alloc\n```cpp\nvoid *GlobalAllocator::do_alloc(size_t size, size_t align);\n``` \nThis function allocates a memory area that is larger than `size` byte and aligned with `align` and returns its address. When implementing this function, the following conditions can be assumed. (heaphook will filter out those that do not meet these conditions.)\n* `size` \u003e 0\n* `align` is either\n  * 1, which means there are no alignment constraints.\n  * a power of 2 and multiple of `sizeof(void *)`, such as 8, 16, 32, ..., 4096, ...\n\nIf memory allocation fails due to various factors, `nullptr` should be returned.\n\nThis function hooks the GLIBC allocation functions `malloc`, `posix_memalign`, `memalign`, `aligned_alloc`, `valloc` and `pvalloc`.\n\n### do_dealloc\n```cpp\nvoid GlobalAllocator::do_dealloc(void *ptr);\n```\nThis function deallocates a memory area pointed to by ptr, which must have been allocated by other allocation functions `do_alloc`, `do_alloc_zeroed` or `do_realloc`. The following conditions can be assumed,\n* `ptr` is the value previously returned from these allocation functions. (If not, the program may be killed.)\n* `ptr` != `nullptr`\n\nThis function hooks the `free` function in GLIBC.\n\n### do_alloc_zeroed\n```cpp\nvoid *GlobalAllocator::do_alloc_zeroed(size_t size);\n``` \nThis function allocates a memory area that is larger than `size` byte. The memory is set to zero. The following conditions can be assumed,\n* `size` \u003e 0\n\nIf memory allocation fails due to various factors, `nullptr` should be returned.\n\nThis function hooks the `calloc` function in GLIBC.\n\nA default implementation is provided that calls `do_alloc` and initializes the allocated memory area with zeros using `memset`.\n\n### do_realloc\n```cpp\nvoid *GlobalAllocator::do_realloc(void *ptr, size_t new_size);\n``` \nThis function changes the size of the memory block pointed to by `ptr` to `new_size` bytes and return a pointer to the new memory area. The contents of the memory area must remain unchanged. When expanging the memory, the added memory does not need to be initialized. The following conditions can be assumed,\n* `ptr` is the value previously returned from these allocation functions. (If not, the program may be killed.)\n* `ptr` != `nullptr`\n* `new_size` \u003e 0\n\nIf memory allocation fails due to various factors, `nullptr` should be returned.\n\nThis function hooks the `realloc` function in GLIBC.\n\nA default implementation is provided that performs `do_alloc(new_size, 1)`, copies contents, and then deallocates the original pointer using `dealloc(ptr)`.\n\n### do_get_block_size\n```cpp\nvoid *GlobalAllocator::do_get_block_size(void *ptr);\n``` \nThis function returns the size of the memory block pointed to by `ptr`. The following conditions can be assumed,\n* `ptr` is the value previously returned from these allocation functions. (If not, the program may be killed.)\n* `ptr` != `nullptr`\n\nThis function is called internally when calling the GLIBC's `malloc_usable_size`.\n\n## Trace function\nheaphook has a trace function for debugging the allocator and analyzing its performance.\n\nIn the CMakeList.txt file, after declaring a new allocator building rule with `build_library`, you can specify `target_compile_options(\u003clibname\u003e PRIVATE \"-DTRACE\")` to build library that traces the allocator's behavior.\n```cmake\nbuild_library(my_allocator ...)\ntarget_compile_options(my_allocator PRIVATE \"-DTRACE\")\n```\nIf configured in this way, the library will generate a log file named `heaplog.\u003cpid\u003e.log` in the current directory. You can visualize heap consumption transitions and performance of each GlobalAllocator member functions in png format based on the generated log file.\n```bash\n$ misc/heaptrace_analyzer.py heaplog.\u003cpid\u003e.log \n```\n\n## Test allocator\nTo test the new memory allocator, add the following statement in CMakeLists.txt. `test_library(\u003ctarget name\u003e \u003csources\u003e..)` is a cmake function which builds a test program based on Google Test.\n```cmake\ntest_library(test_my_allocator\n  src/original_allocator.cpp)\n```\nAfter describing the above, colcon build will create a test program in build/heaphook/test_my_allocator. The source of this test program is in the file test/test_allocator.cpp, so please refer to it if necessary.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftier4%2Fheaphook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftier4%2Fheaphook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftier4%2Fheaphook/lists"}