1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

Merge pull request #20363 from mguetschow/malloc-monitor

Monitoring malloc usage
This commit is contained in:
Marian Buschsieweke 2024-03-26 10:58:33 +00:00 committed by GitHub
commit a9d052bc32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 656 additions and 2 deletions

View File

@ -259,7 +259,11 @@ PSEUDOMODULES += libc_gettimeofday
## @defgroup pseudomodule_malloc_tracing malloc_tracing
## @brief Debug dynamic memory management by hooking in a print into each call
## of malloc(), calloc(), realloc() and free
## @{
## @deprecated Use module `malloc_monitor` with verbous configuration instead;
## will be removed after 2024.07 release.
PSEUDOMODULES += malloc_tracing
## @}
## @defgroup pseudomodule_mpu_stack_guard mpu_stack_guard
## @brief MPU based stack guard

View File

@ -79,6 +79,7 @@ rsource "evtimer/Kconfig"
rsource "log_color/Kconfig"
rsource "log_printfnoformat/Kconfig"
rsource "luid/Kconfig"
rsource "malloc_monitor/Kconfig"
rsource "malloc_thread_safe/Kconfig"
rsource "matstat/Kconfig"
rsource "memarray/Kconfig"

View File

@ -0,0 +1,59 @@
/*
* Copyright (C) 2024 TU Dresden
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup sys_malloc_monitor
* @{
*/
#ifndef MALLOC_MONITOR_H
#define MALLOC_MONITOR_H
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "architecture.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Obtain current heap memory usage.
*
* @return current heap memory usage in bytes
*/
size_t malloc_monitor_get_usage_current(void);
/**
* @brief Obtain maximum heap memory usage since last call to
* @ref malloc_monitor_reset_high_watermark().
*
* @return maximum heap memory usage in bytes
*/
size_t malloc_monitor_get_usage_high_watermark(void);
/**
* @brief Reset maximum heap memory usage.
*
* After calling this function, @ref malloc_monitor_get_usage_high_watermark()
* will return @ref malloc_monitor_get_usage_current() until further changes
* to heap memory usage.
*/
void malloc_monitor_reset_high_watermark(void);
#ifdef __cplusplus
}
#endif
#endif /* MALLOC_MONITOR_H */
/**
* @}
*/

View File

@ -0,0 +1,73 @@
/*
* Copyright (C) 2024 TU Dresden
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @defgroup sys_malloc_monitor_internals Heap Memory Usage Monitor internals
* @ingroup sys_malloc_monitor
* @{
*
* @brief internals for monitoring heap memory usage (calls to malloc/calloc/realloc/free)
* @author Mikolai Gütschow <mikolai.guetschow@tu-dresden.de>
*/
#ifndef MALLOC_MONITOR_INTERNAL_H
#define MALLOC_MONITOR_INTERNAL_H
#include <assert.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "architecture.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Record malloc/calloc/realloc call increasing heap usage.
*
* @param[in] ptr pointer to newly allocated memory
* @param[in] size size of newly allocated memory
* @param[in] pc PC of calling function
* @param[in] func_prefix prefix identifying memory function, one of "m","c","re"
*
* @internal
*/
void malloc_monitor_add(void *ptr, size_t size, uinttxtptr_t pc, char *func_prefix);
/**
* @brief Record free/realloc call decreasing heap usage.
*
* @param[in] ptr pointer to memory that is being freed
* @param[in] pc PC of calling function
*
* @internal
*/
void malloc_monitor_rm(void *ptr, uinttxtptr_t pc);
/**
* @brief Record realloc call either increasing or decreasing heap usage.
*
* @param[in] ptr_old pointer to previously allocated memory
* @param[in] ptr_new pointer to newly allocated memory
* @param[in] size_new size of newly allocated memory
* @param[in] pc PC of calling function
*
* @internal
*/
void malloc_monitor_mv(void *ptr_old, void *ptr_new, size_t size_new, uinttxtptr_t pc);
#ifdef __cplusplus
}
#endif
#endif /* MALLOC_MONITOR_INTERNAL_H */
/**
* @}
*/

View File

@ -0,0 +1,23 @@
# Copyright (C) 2024 TU Dresden
#
# This file is subject to the terms and conditions of the GNU Lesser
# General Public License v2.1. See the file LICENSE in the top level
# directory for more details.
#
menuconfig MODULE_SYS_MALLOC_MONITOR
bool "Heap Memory Usage Monitor"
config MODULE_SYS_MALLOC_MONITOR_SIZE
int "Monitor Size"
default 100
depends on MODULE_SYS_MALLOC_MONITOR
help
Specifies maximum number of pointers that can be monitored at once.
config MODULE_SYS_MALLOC_MONITOR_VERBOSE
bool "Verbose"
default false
depends on MODULE_SYS_MALLOC_MONITOR
help
Print detailed log of calls to malloc/realloc/free

View File

@ -0,0 +1 @@
include $(RIOTBASE)/Makefile.base

View File

@ -0,0 +1 @@
USEMODULE += malloc_thread_safe

View File

@ -0,0 +1,72 @@
/**
* @defgroup sys_malloc_monitor Heap Memory Usage Monitor
* @ingroup sys_memory_management
* @brief This module allows to monitor the dynamic memory usage of a certain piece of code.
* @warning This module automatically selects @ref sys_malloc_ts and naturally
* incurs a certain runtime overhead. It is not meant for production usage.
* @author Mikolai Gütschow <mikolai.guetschow@tu-dresden.de>
*
* # Description
*
* This module allows to monitor the dynamic memory usage of a certain piece of code.
* It works by hooking into (wrappers to) @ref malloc(), @ref calloc(), @ref realloc(),
* and @ref free() calls to internally record the current and all-time maximum heap memory usage.
*
* Note that in general dynamic memory management is a bad idea on the constrained devices RIOT
* is targeting. So maybe it is better to just adapt your code to use static memory management instead.
*
* # Usage
*
* Enable the module with `USEMODULE += malloc_monitor`.
*
* Add `#include "malloc_monitor.h"` to the file in which you want to monitor dynamic memory usage.
* Use @ref malloc_monitor_get_usage_current() to retrieve the size of the currently allocated
* heap memory in bytes. @ref malloc_monitor_get_usage_high_watermark() returns the all-time maximum
* since startup or the last call to @ref malloc_monitor_reset_high_watermark().
*
* Note that `malloc_monitor` currently has no notion of threads and will at any point in time report
* the global dynamic memory usage, not the one used by the currently running thread.
* Thread-safety is achieved through usage of @ref sys_malloc_ts, though.
*
* ## Example
*
* Imagine you want to investigate the dynamic memory consumption of a certain function `func()`.
* The following snippet could get you started:
*
* ```c
* #include <stddef.h>
* #include <stdio.h>
*
* #include "malloc_monitor.h"
*
* int main(void)
* {
* size_t before = malloc_monitor_get_usage_current();
* size_t before_max = malloc_monitor_get_usage_high_watermark();
* func();
* size_t after = malloc_monitor_get_usage_current();
* size_t after_max = malloc_monitor_get_usage_high_watermark();
*
* if (after != before) {
* puts("func() " (after < before ? "decreased" : "increased") " global dynamic memory usage.");
* }
* printf("The maximal dynamic memory usage of func() was %d bytes.", after_max - before_max);
* }
* ```
*
* For further usage examples, refer to the corresponding tests in `tests/sys/malloc_monitor`.
*
* # Configuration
*
* The maximum number of pointers that can be monitored at once can be set with Kconfig
* in System > Heap Memory Usage Monitor > Monitor Size or by setting the corresponding
* CFlag in your application's Makefile as `CFLAGS += CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE=42`.
* It defaults to 100.
*
* For more fine-grained debugging of invalid calls to @ref free(), duplicated calls to @ref free(),
* or memory leaks, the module can be configured to print information on every call to @ref malloc(),
* @ref calloc(), @ref realloc(), or @ref free() by setting System > Heap Memory Usage Monitor > Verbose
* or adding `CFLAGS += CONFIG_MODULE_SYS_MALLOC_MONITOR_VERBOSE=1` to your Makefile.
* `malloc_monitor` defaults to be non-verbose.
*
*/

View File

@ -0,0 +1,170 @@
/*
* Copyright (C) 2024 TU Dresden
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @{
*
* @file
* @brief monitor heap memory usage (calls to malloc/realloc/free)
* @author Mikolai Gütschow <mikolai.guetschow@tu-dresden.de>
*/
#include <stdio.h>
#include <string.h>
#include "architecture.h"
#include "assert.h"
#include "cpu.h"
#include "irq.h"
#include "mutex.h"
#include "malloc_monitor.h"
#include "malloc_monitor_internal.h"
#ifndef CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE
#define CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE 100
#endif
#ifndef CONFIG_MODULE_SYS_MALLOC_MONITOR_VERBOSE
#define CONFIG_MODULE_SYS_MALLOC_MONITOR_VERBOSE 0
#endif
static struct {
void *addr[CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE];
size_t size[CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE];
size_t current;
size_t high_watermark;
} malloc_monitor = {
.addr = {NULL},
.current = 0,
.high_watermark = 0,
};
/* guards access to malloc_monitor */
static mutex_t _lock;
void malloc_monitor_add(void *ptr, size_t size, uinttxtptr_t pc, char *func_prefix)
{
if (ptr == NULL) {
return;
}
#if CONFIG_MODULE_SYS_MALLOC_MONITOR_VERBOSE
printf("%salloc(%" PRIuSIZE ") @ 0x%" PRIxTXTPTR " returned %p\n",
func_prefix, size, pc, ptr);
#endif
assert(!irq_is_in());
mutex_lock(&_lock);
for (uint8_t i=0; i<CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE; i++) {
if (malloc_monitor.addr[i] == NULL) {
malloc_monitor.addr[i] = ptr;
malloc_monitor.size[i] = size;
malloc_monitor.current += size;
if (malloc_monitor.current > malloc_monitor.high_watermark) {
malloc_monitor.high_watermark = malloc_monitor.current;
}
mutex_unlock(&_lock);
return;
}
}
mutex_unlock(&_lock);
printf("malloc_monitor: maximum number of pointers to be monitored "
"(as set by CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE) exceeded.\n");
(void)func_prefix;
(void)pc;
}
void malloc_monitor_rm(void *ptr, uinttxtptr_t pc)
{
if (ptr == NULL) {
return;
}
#if CONFIG_MODULE_SYS_MALLOC_MONITOR_VERBOSE
printf("malloc_monitor: free(%p) @ 0x%" PRIxTXTPTR " \n", ptr, pc);
#endif
assert(!irq_is_in());
mutex_lock(&_lock);
for (uint8_t i=0; i<CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE; i++) {
if (malloc_monitor.addr[i] == ptr) {
malloc_monitor.addr[i] = NULL;
malloc_monitor.current -= malloc_monitor.size[i];
mutex_unlock(&_lock);
return;
}
}
mutex_unlock(&_lock);
printf("malloc_monitor: free(%p) @ 0x%" PRIxTXTPTR " invalid\n", ptr, pc);
}
void malloc_monitor_mv(void *ptr_old, void *ptr_new, size_t size_new, uinttxtptr_t pc)
{
if (ptr_old == NULL) {
malloc_monitor_add(ptr_new, size_new, pc, "re");
return;
}
if (size_new == 0) {
malloc_monitor_rm(ptr_old, pc);
return;
}
if (ptr_new == NULL) {
return;
}
#if CONFIG_MODULE_SYS_MALLOC_MONITOR_VERBOSE
printf("malloc_monitor: realloc(%p, %" PRIuSIZE ") @0x%" PRIxTXTPTR " returned %p\n",
ptr_old, size_new, pc, ptr_new);
#endif
assert(!irq_is_in());
mutex_lock(&_lock);
for (uint8_t i=0; i<CONFIG_MODULE_SYS_MALLOC_MONITOR_SIZE; i++) {
if (malloc_monitor.addr[i] == ptr_old) {
malloc_monitor.addr[i] = ptr_new;
size_t size_old = malloc_monitor.size[i];
malloc_monitor.size[i] = size_new;
if (size_new > size_old) {
malloc_monitor.current += size_new - size_old;
if (malloc_monitor.current > malloc_monitor.high_watermark) {
malloc_monitor.high_watermark = malloc_monitor.current;
}
}
else {
malloc_monitor.current -= size_old - size_new;
}
mutex_unlock(&_lock);
return;
}
}
mutex_unlock(&_lock);
printf("malloc_monitor: realloc(%p) @ 0x%" PRIxTXTPTR " invalid\n", ptr_old, pc);
}
size_t malloc_monitor_get_usage_current(void)
{
assert(!irq_is_in());
mutex_lock(&_lock);
size_t ret = malloc_monitor.current;
mutex_unlock(&_lock);
return ret;
}
size_t malloc_monitor_get_usage_high_watermark(void)
{
assert(!irq_is_in());
mutex_lock(&_lock);
size_t ret = malloc_monitor.high_watermark;
mutex_unlock(&_lock);
return ret;
}
void malloc_monitor_reset_high_watermark(void)
{
assert(!irq_is_in());
mutex_lock(&_lock);
malloc_monitor.high_watermark = malloc_monitor.current;
mutex_unlock(&_lock);
}
/** @} */

View File

@ -1,6 +1,6 @@
/**
@defgroup sys_malloc_ts Thread-safe wrappers for malloc and friends
@ingroup sys
@ingroup sys_memory_management
@brief This module provides wrappers for malloc, calloc, realloc and free
that provide mutually exclusive access to those functions.
@warning This module is automatically selected, if needed. Never add it

View File

@ -24,6 +24,7 @@
#include "cpu.h"
#include "irq.h"
#include "kernel_defines.h"
#include "malloc_monitor_internal.h"
#include "mutex.h"
extern void *__real_malloc(size_t size);
@ -41,6 +42,9 @@ void __attribute__((used)) *__wrap_malloc(size_t size)
assert(!irq_is_in());
mutex_lock(&_lock);
void *ptr = __real_malloc(size);
if (IS_USED(MODULE_MALLOC_MONITOR)) {
malloc_monitor_add(ptr, size, cpu_get_caller_pc(), "m");
}
mutex_unlock(&_lock);
if (IS_USED(MODULE_MALLOC_TRACING)) {
printf("malloc(%" PRIuSIZE ") @ 0x%" PRIxTXTPTR " returned %p\n",
@ -58,6 +62,9 @@ void __attribute__((used)) __wrap_free(void *ptr)
assert(!irq_is_in());
mutex_lock(&_lock);
__real_free(ptr);
if (IS_USED(MODULE_MALLOC_MONITOR)) {
malloc_monitor_rm(ptr, cpu_get_caller_pc());
}
mutex_unlock(&_lock);
}
@ -81,6 +88,9 @@ void * __attribute__((used)) __wrap_calloc(size_t nmemb, size_t size)
mutex_lock(&_lock);
void *res = __real_malloc(total_size);
if (IS_USED(MODULE_MALLOC_MONITOR)) {
malloc_monitor_add(res, total_size, cpu_get_caller_pc(), "c");
}
mutex_unlock(&_lock);
if (res) {
memset(res, 0, total_size);
@ -104,6 +114,9 @@ void * __attribute__((used))__wrap_realloc(void *ptr, size_t size)
assert(!irq_is_in());
mutex_lock(&_lock);
void *new = __real_realloc(ptr, size);
if (IS_USED(MODULE_MALLOC_MONITOR)) {
malloc_monitor_mv(ptr, new, size, cpu_get_caller_pc());
}
mutex_unlock(&_lock);
if (IS_USED(MODULE_MALLOC_TRACING)) {

View File

@ -0,0 +1,7 @@
include ../Makefile.sys_common
USEMODULE += embunit
USEMODULE += malloc_monitor
include $(RIOTBASE)/Makefile.include

View File

@ -0,0 +1,4 @@
BOARD_INSUFFICIENT_MEMORY := \
atmega8 \
nucleo-l011k4 \
#

View File

@ -0,0 +1,212 @@
/*
* Copyright (C) 2024 TU Dresden
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup tests
* @{
*
* @file
* @brief Test application for MODULE_MALLOC_MONITOR
*
* @author Mikolai Gütschow <mikolai.guetschow@tu-dresden.de>
* @}
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include "embUnit.h"
#include "malloc_monitor.h"
#define MALLOC_SIZE 1
#define TEST_MALLOC_MONITOR_SAVE \
size_t curr = malloc_monitor_get_usage_current(); \
size_t water = malloc_monitor_get_usage_high_watermark();
#define TEST_ASSERT_CURRENT(num_alloc) TEST_ASSERT_EQUAL_INT(curr+num_alloc*MALLOC_SIZE, malloc_monitor_get_usage_current());
#define TEST_ASSERT_WATERMARK(num_alloc) TEST_ASSERT_EQUAL_INT(water+num_alloc*MALLOC_SIZE, malloc_monitor_get_usage_high_watermark());
/*
* malloc and free should be reflected by `malloc_monitor_get_usage_current()`.
* `malloc_monitor_get_usage_high_watermark()` should only be decreased on a call to
* `malloc_monitor_reset_high_watermark()`
*/
static void test_malloc_free(void)
{
TEST_MALLOC_MONITOR_SAVE
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(0);
void *volatile alloc1 = malloc(MALLOC_SIZE);
TEST_ASSERT_NOT_NULL(alloc1);
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(1);
void *volatile alloc2 = malloc(MALLOC_SIZE);
TEST_ASSERT_NOT_NULL(alloc2);
TEST_ASSERT_CURRENT(2);
TEST_ASSERT_WATERMARK(2);
free(alloc1);
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(2);
malloc_monitor_reset_high_watermark();
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(1);
free(alloc2);
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(1);
malloc_monitor_reset_high_watermark();
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(0);
}
/*
* using calloc instead of malloc should be reflected correctly
*/
static void test_calloc(void)
{
TEST_MALLOC_MONITOR_SAVE
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(0);
void *volatile alloc1 = calloc(1, MALLOC_SIZE);
TEST_ASSERT_NOT_NULL(alloc1);
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(1);
void *volatile alloc2 = calloc(2, MALLOC_SIZE);
TEST_ASSERT_NOT_NULL(alloc2);
TEST_ASSERT_CURRENT(3);
TEST_ASSERT_WATERMARK(3);
free(alloc1);
TEST_ASSERT_CURRENT(2);
TEST_ASSERT_WATERMARK(3);
malloc_monitor_reset_high_watermark();
TEST_ASSERT_CURRENT(2);
TEST_ASSERT_WATERMARK(2);
free(alloc2);
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(2);
malloc_monitor_reset_high_watermark();
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(0);
}
/*
* using realloc instead of malloc/free should be reflected correctly
*/
static void test_realloc(void)
{
TEST_MALLOC_MONITOR_SAVE
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(0);
void *volatile alloc1 = realloc(NULL, MALLOC_SIZE);
TEST_ASSERT_NOT_NULL(alloc1);
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(1);
alloc1 = realloc(alloc1, 2*MALLOC_SIZE);
TEST_ASSERT_NOT_NULL(alloc1);
TEST_ASSERT_CURRENT(2);
TEST_ASSERT_WATERMARK(2);
alloc1 = realloc(alloc1, 1*MALLOC_SIZE);
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(2);
malloc_monitor_reset_high_watermark();
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(1);
alloc1 = realloc(alloc1, 0*MALLOC_SIZE);
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(1);
malloc_monitor_reset_high_watermark();
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(0);
}
/*
* freeing NULL shouldn't change anything
*/
static void test_free_NULL(void)
{
TEST_MALLOC_MONITOR_SAVE
free(NULL);
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(0);
void *volatile alloc1 = malloc(MALLOC_SIZE);
TEST_ASSERT_NOT_NULL(alloc1);
TEST_ASSERT_CURRENT(1);
TEST_ASSERT_WATERMARK(1);
free(alloc1);
TEST_ASSERT_CURRENT(0);
TEST_ASSERT_WATERMARK(1);
}
static Test *tests_malloc_monitor(void)
{
EMB_UNIT_TESTFIXTURES(fixtures) {
new_TestFixture(test_malloc_free),
new_TestFixture(test_calloc),
new_TestFixture(test_realloc),
new_TestFixture(test_free_NULL),
};
EMB_UNIT_TESTCALLER(tests, NULL, NULL, fixtures);
return (Test *)&tests;
}
int main(void)
{
puts("malloc_monitor test");
TESTS_START();
TESTS_RUN(tests_malloc_monitor());
TESTS_END();
return 0;
}

View File

@ -0,0 +1,14 @@
#!/usr/bin/env python3
# Copyright (C) 2024 TU Dresden
#
# This file is subject to the terms and conditions of the GNU Lesser
# General Public License v2.1. See the file LICENSE in the top level
# directory for more details.
import sys
from testrunner import run_check_unittests
if __name__ == "__main__":
sys.exit(run_check_unittests())