Skip to content

Commit

Permalink
Generalized Mem tracing (#536)
Browse files Browse the repository at this point in the history
* Ported alloc tracer from Java CRT, added unit tests

* Generalized backtrace/backtrace_symbols for multiple platforms

* Added tests to ensure midstream usage works

* Stack trace decoding test now uses virtual logger

* Fixed AWS_VARIABLE_LENGTH_ARRAY on windows, causing stack corruption

* Converted all tests to use the mem tracer

* Removed allocator from test harness structure, enabled logging by default
  • Loading branch information
Justin Boswell authored Nov 5, 2019
1 parent e3e7ccd commit cbf339c
Show file tree
Hide file tree
Showing 18 changed files with 1,175 additions and 166 deletions.
54 changes: 54 additions & 0 deletions include/aws/common/allocator.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,60 @@ int aws_mem_realloc(struct aws_allocator *allocator, void **ptr, size_t oldsize,
* that we can leave unchanged on failure.
*/

enum aws_mem_trace_level {
AWS_MEMTRACE_NONE = 0, /* no tracing */
AWS_MEMTRACE_BYTES = 1, /* just track allocation sizes and total allocated */
AWS_MEMTRACE_STACKS = 2, /* capture callstacks for each allocation */
};

#if defined(AWS_HAVE_EXECINFO) || defined(WIN32) || defined(__APPLE__)
# define AWS_MEMTRACE_STACKS_AVAILABLE
#endif

/*
* Wraps an allocator and tracks all external allocations. If aws_mem_trace_dump() is called
* and there are still allocations active, they will be reported to the aws_logger at TRACE level.
* allocator - The allocator to wrap
* system_allocator - The allocator to allocate bookkeeping data from, or NULL to use the default
* level - The level to track allocations at
* frames_per_stack is how many frames to store per callstack if AWS_MEMTRACE_STACKS is in use,
* otherwise it is ignored. 8 tends to be a pretty good number balancing storage space vs useful stacks.
* Returns the tracer allocator, which should be used for all allocations that should be tracked.
*/
AWS_COMMON_API
struct aws_allocator *aws_mem_tracer_new(
struct aws_allocator *allocator,
struct aws_allocator *system_allocator,
enum aws_mem_trace_level level,
size_t frames_per_stack);

/*
* Unwraps the traced allocator and cleans up the tracer.
* Returns the original allocator
*/
AWS_COMMON_API
struct aws_allocator *aws_mem_tracer_destroy(struct aws_allocator *trace_allocator);

/*
* If there are outstanding allocations, dumps them to log, along with any information gathered
* based on the trace level set when aws_mem_trace() was called.
* Should be passed the tracer allocator returned from aws_mem_trace().
*/
AWS_COMMON_API
void aws_mem_tracer_dump(struct aws_allocator *trace_allocator);

/*
* Returns the current number of bytes in outstanding allocations
*/
AWS_COMMON_API
size_t aws_mem_tracer_bytes(struct aws_allocator *trace_allocator);

/*
* Returns the current number of outstanding allocations
*/
AWS_COMMON_API
size_t aws_mem_tracer_count(struct aws_allocator *trace_allocator);

AWS_EXTERN_C_END

#endif /* AWS_COMMON_ALLOCATOR_H */
7 changes: 0 additions & 7 deletions include/aws/common/assert.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,6 @@ AWS_COMMON_API
AWS_DECLSPEC_NORETURN
void aws_fatal_assert(const char *cond_str, const char *file, int line) AWS_ATTRIBUTE_NORETURN;

/**
* Print a backtrace from either the current stack, or (if provided) the current exception/signal
* call_site_data is siginfo_t* on POSIX, and LPEXCEPTION_POINTERS on Windows, and can be null
*/
AWS_COMMON_API
void aws_backtrace_print(FILE *fp, void *call_site_data);

AWS_EXTERN_C_END

#if defined(CBMC)
Expand Down
1 change: 1 addition & 0 deletions include/aws/common/logging.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct aws_log_subject_info_list {
enum aws_common_log_subject {
AWS_LS_COMMON_GENERAL = 0,
AWS_LS_COMMON_TASK_SCHEDULER,
AWS_LS_COMMON_MEMTRACE,

AWS_LS_COMMON_LAST = (AWS_LS_COMMON_GENERAL + AWS_LOG_SUBJECT_SPACE_SIZE - 1)
};
Expand Down
4 changes: 2 additions & 2 deletions include/aws/common/macros.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ AWS_STATIC_ASSERT(CALL_OVERLOAD_TEST(1, 2, 3) == 3);
# define AWS_LIKELY(x) x
# define AWS_UNLIKELY(x) x
# define AWS_FORCE_INLINE __forceinline
# define AWS_VARIABLE_LENGTH_ARRAY(type, name, length) type *name = _alloca(sizeof(type) * length)
# define AWS_VARIABLE_LENGTH_ARRAY(type, name, length) type *name = _alloca(sizeof(type) * (length))
# define AWS_DECLSPEC_NORETURN __declspec(noreturn)
# define AWS_ATTRIBUTE_NORETURN
#else
Expand All @@ -76,7 +76,7 @@ AWS_STATIC_ASSERT(CALL_OVERLOAD_TEST(1, 2, 3) == 3);
# define AWS_DECLSPEC_NORETURN
# define AWS_ATTRIBUTE_NORETURN __attribute__((noreturn))
# if defined(__cplusplus)
# define AWS_VARIABLE_LENGTH_ARRAY(type, name, length) type *name = alloca(sizeof(type) * length)
# define AWS_VARIABLE_LENGTH_ARRAY(type, name, length) type *name = alloca(sizeof(type) * (length))
# else
# define AWS_VARIABLE_LENGTH_ARRAY(type, name, length) type name[length];
# endif /* defined(__cplusplus) */
Expand Down
44 changes: 44 additions & 0 deletions include/aws/common/system_info.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,50 @@ bool aws_is_debugger_present(void);
AWS_COMMON_API
void aws_debug_break(void);

#if defined(AWS_HAVE_EXECINFO) || defined(WIN32) || defined(__APPLE__)
# define AWS_BACKTRACE_STACKS_AVAILABLE
#endif

/*
* Records a stack trace from the call site.
* Returns the number of stack entries/stack depth captured, or 0 if the operation
* is not supported on this platform
*/
AWS_COMMON_API
size_t aws_backtrace(void **frames, size_t num_frames);

/*
* Converts stack frame pointers to symbols, if symbols are available
* Returns an array up to stack_depth long, that needs to be free()ed.
* stack_depth should be the length of frames.
* Returns NULL if the platform does not support stack frame translation
* or an error occurs
*/
char **aws_backtrace_symbols(void *const *frames, size_t stack_depth);

/*
* Converts stack frame pointers to symbols, using all available system
* tools to try to produce a human readable result. This call will not be
* quick, as it shells out to addr2line or similar tools.
* On Windows, this is the same as aws_backtrace_symbols()
* Returns an array up to stack_depth long that needs to be free()ed. Missing
* frames will be NULL.
* Returns NULL if the platform does not support stack frame translation
* or an error occurs
*/
char **aws_backtrace_addr2line(void *const *frames, size_t stack_depth);

/**
* Print a backtrace from either the current stack, or (if provided) the current exception/signal
* call_site_data is siginfo_t* on POSIX, and LPEXCEPTION_POINTERS on Windows, and can be null
*/
AWS_COMMON_API
void aws_backtrace_print(FILE *fp, void *call_site_data);

/* Log the callstack from the current stack to the currently configured aws_logger */
AWS_COMMON_API
void aws_backtrace_log(void);

AWS_EXTERN_C_END

#endif /* AWS_COMMON_SYSTEM_INFO_H */
110 changes: 36 additions & 74 deletions include/aws/testing/aws_test_harness.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

#include <aws/common/common.h>
#include <aws/common/error.h>
#include <aws/common/logging.h>
#include <aws/common/mutex.h>
#include <aws/common/system_info.h>

#include <stdarg.h>
#include <stdio.h>
Expand All @@ -39,46 +41,6 @@ the AWS_UNSTABLE_TESTING_API compiler flag
# pragma warning(disable : 4204) /* non-constant aggregate initializer */
#endif

struct memory_test_allocator {
size_t allocated;
size_t freed;
struct aws_mutex mutex;
};

struct memory_test_tracker {
size_t size;
void *blob;
};

static inline void *s_mem_acquire_malloc(struct aws_allocator *allocator, size_t size) {
struct memory_test_allocator *test_allocator = (struct memory_test_allocator *)allocator->impl;

aws_mutex_lock(&test_allocator->mutex);
test_allocator->allocated += size;
struct memory_test_tracker *memory =
(struct memory_test_tracker *)malloc(size + sizeof(struct memory_test_tracker));

if (!memory) {
return NULL;
}

memory->size = size;
memory->blob = (uint8_t *)memory + sizeof(struct memory_test_tracker);
aws_mutex_unlock(&test_allocator->mutex);
return memory->blob;
}

static inline void s_mem_release_free(struct aws_allocator *allocator, void *ptr) {
struct memory_test_allocator *test_allocator = (struct memory_test_allocator *)allocator->impl;

struct memory_test_tracker *memory =
(struct memory_test_tracker *)((uint8_t *)ptr - sizeof(struct memory_test_tracker));
aws_mutex_lock(&test_allocator->mutex);
test_allocator->freed += memory->size;
aws_mutex_unlock(&test_allocator->mutex);
free(memory);
}

/** Prints a message to stdout using printf format that appends the function, file and line number.
* If format is null, returns 0 without printing anything; otherwise returns 1.
*/
Expand Down Expand Up @@ -342,7 +304,6 @@ struct aws_test_harness {
aws_test_before_fn *on_before;
aws_test_run_fn *run;
aws_test_after_fn *on_after;
struct aws_allocator *allocator;
void *ctx;
const char *test_name;
int suppress_memcheck;
Expand Down Expand Up @@ -385,34 +346,53 @@ static inline int s_aws_run_test_case(struct aws_test_harness *harness) {
sigaction(SIGSEGV, &sa, NULL);
#endif

/* track allocations and report leaks in tests, unless suppressed */
struct aws_allocator *allocator = NULL;
if (harness->suppress_memcheck) {
allocator = aws_default_allocator();
} else {
allocator = aws_mem_tracer_new(aws_default_allocator(), NULL, AWS_MEMTRACE_STACKS, 8);
}

/* wire up a logger to stderr by default, may be replaced by some tests */
struct aws_logger err_logger;
struct aws_logger_standard_options options;
options.file = AWS_TESTING_REPORT_FD;
options.level = AWS_LL_TRACE;
options.filename = NULL;
aws_logger_init_standard(&err_logger, aws_default_allocator(), &options);
aws_logger_set(&err_logger);

if (harness->on_before) {
harness->on_before(harness->allocator, harness->ctx);
harness->on_before(allocator, harness->ctx);
}

int ret_val = harness->run(harness->allocator, harness->ctx);
int ret_val = harness->run(allocator, harness->ctx);

if (harness->on_after) {
harness->on_after(harness->allocator, harness->ctx);
harness->on_after(allocator, harness->ctx);
}

if (!ret_val) {
if (!harness->suppress_memcheck) {
struct memory_test_allocator *alloc_impl = (struct memory_test_allocator *)harness->allocator->impl;
ASSERT_UINT_EQUALS(
alloc_impl->allocated,
alloc_impl->freed,
"%s [ \033[31mFAILED\033[0m ]"
"Memory Leak Detected %d bytes were allocated, "
"but only %d were freed.",
harness->test_name,
alloc_impl->allocated,
alloc_impl->freed);
const size_t leaked_bytes = aws_mem_tracer_count(allocator);
if (leaked_bytes) {
aws_mem_tracer_dump(allocator);
}
ASSERT_UINT_EQUALS(0, aws_mem_tracer_count(allocator));
}
}

/* clean up */
aws_mem_tracer_destroy(allocator);
aws_logger_set(NULL);
aws_logger_clean_up(&err_logger);

if (!ret_val) {
RETURN_SUCCESS("%s [ \033[32mOK\033[0m ]", harness->test_name);
} else {
FAIL("%s [ \033[31mFAILED\033[0m ]", harness->test_name);
}

FAIL("%s [ \033[31mFAILED\033[0m ]", harness->test_name);
}

/* Enables terminal escape sequences for text coloring on Windows. */
Expand Down Expand Up @@ -451,28 +431,12 @@ static inline int enable_vt_mode(void) {

#endif

#define AWS_TEST_ALLOCATOR_INIT(name) \
static struct memory_test_allocator name##_alloc_impl = { \
0, \
0, \
AWS_MUTEX_INIT, \
}; \
static struct aws_allocator name##_allocator = { \
s_mem_acquire_malloc, \
s_mem_release_free, \
NULL, \
NULL, \
&name##_alloc_impl, \
};

#define AWS_TEST_CASE_SUPRESSION(name, fn, s) \
static int fn(struct aws_allocator *allocator, void *ctx); \
AWS_TEST_ALLOCATOR_INIT(name) \
static struct aws_test_harness name##_test = { \
NULL, \
fn, \
NULL, \
&name##_allocator, \
NULL, \
#name, \
s, \
Expand All @@ -486,12 +450,10 @@ static inline int enable_vt_mode(void) {
static void b(struct aws_allocator *allocator, void *ctx); \
static int fn(struct aws_allocator *allocator, void *ctx); \
static void af(struct aws_allocator *allocator, void *ctx); \
AWS_TEST_ALLOCATOR_INIT(name) \
static struct aws_test_harness name##_test = { \
b, \
fn, \
af, \
&name##_allocator, \
c, \
#name, \
s, \
Expand Down
4 changes: 2 additions & 2 deletions source/assert.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@

#include <aws/common/common.h>

#include <aws/common/system_info.h>

#include <stdio.h>
#include <stdlib.h>

void aws_debug_break(void);

void aws_fatal_assert(const char *cond_str, const char *file, int line) {
aws_debug_break();
fprintf(stderr, "Fatal error condition occurred in %s:%d: %s\nExiting Application\n", file, line, cond_str);
Expand Down
1 change: 1 addition & 0 deletions source/common.c
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ static struct aws_log_subject_info s_common_log_subject_infos[] = {
AWS_LS_COMMON_TASK_SCHEDULER,
"task-scheduler",
"Subject for task scheduler or task specific logging."),
DEFINE_LOG_SUBJECT_INFO(AWS_LS_COMMON_MEMTRACE, "memtrace", "Output from the aws_mem_trace_dump function"),
};

static struct aws_log_subject_info_list s_common_log_subject_list = {
Expand Down
Loading

0 comments on commit cbf339c

Please sign in to comment.