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

@ -30,4 +30,4 @@ config MODULE_MALLOC_TRACING
Note that generally 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.
adapt your code to use static memory management instead.

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())