From 4fe4e293ad5f600cffb3436e95f0c5ce0f1941e1 Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Tue, 9 Sep 2025 16:03:25 -0700 Subject: [PATCH] Add tree walking debug function --- include/refcount/refcount.h | 28 ++++++++++++++ src/refcount.c | 73 +++++++++++++++++++++++++++++++++---- test/test_refcount.c | 12 ++++++ 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/include/refcount/refcount.h b/include/refcount/refcount.h index 7d4cb06..ae8b7d7 100644 --- a/include/refcount/refcount.h +++ b/include/refcount/refcount.h @@ -457,6 +457,34 @@ static inline bool refcount_remove_destructor(void *obj, void *key) { } // Debug Functions +/** + * Callback used from #refcount_debug_context_walk_tree. + * @param cur The current object in the walk + * @param trail The parent objects, up to the root object + * @return True if the walk should stop, false otherwise + */ +typedef bool (*refcount_debug_walk_callback_t)(void *cur, + const RefcountList *trail, + void *user_data); + +bool refcount_debug_context_walk_tree(const RefcountContext *ctx, void *obj, + refcount_debug_walk_callback_t callback, + void *user_data); + +/** + * Same as #refcount_debug_context_walk_tree, but only operates on the global + * context. + * @param obj The root object + * @param callback The callback + * @param user_data Extra data to pass to the callback + * @return True if the walk ended early because a callback returned true, false + */ +static inline bool +refcount_debug_walk_tree(void *obj, refcount_debug_walk_callback_t callback, + void *user_data) { + return refcount_debug_context_walk_tree(refcount_default_context, obj, + callback, user_data); +} uint64_t refcount_debug_context_count_object(const RefcountContext *ctx, void *obj, void *target); diff --git a/src/refcount.c b/src/refcount.c index ad42a50..21f893b 100644 --- a/src/refcount.c +++ b/src/refcount.c @@ -846,6 +846,70 @@ bool refcount_context_remove_destructor(const RefcountContext *ctx, void *obj, } // Debug Functions +static const HTTableFunctions DEBUG_SEEN_FNS = { + .destroy_key = NULL, + .destroy_value = NULL, + .equal = ht_intptr_equal_callback, + .hash = ht_intptr_hash_callback, + .user_data = NULL, +}; + +/** + * Walk the tree held references tree of a given object, calling a callback for + * each object found. The root object will also be called. This is for debug + * purposes only. + * @param ctx The #RefcountContext + * @param obj The root object + * @param callback The callback + * @param user_data Extra data to pass to the callback + * @return True if the walk ended early because a callback returned true, false + * otherwise + */ +bool refcount_debug_context_walk_tree(const RefcountContext *ctx, void *obj, + refcount_debug_walk_callback_t callback, + void *user_data) { + static int frame_start_mark, frame_end_mark; + if (!obj) { + return false; + } + RefcountList *queue = NULL; + queue = refcount_list_push_full(queue, obj, &ctx->alloc); + HTTable *seen = ht_new(&DEBUG_SEEN_FNS, &ctx->ht_alloc, NULL); + RefcountList *trail = NULL; + while (queue) { + void *cur = queue->data; + queue = refcount_list_pop_full(queue, NULL, &ctx->alloc); + if (cur == &frame_start_mark) { + void *to_add = queue->data; + trail = refcount_list_push_full(trail, to_add, &ctx->alloc); + queue = refcount_list_pop_full(queue, NULL, &ctx->alloc); + } else if (cur == &frame_end_mark) { + trail = refcount_list_pop_full(trail, NULL, &ctx->alloc); + } else { + if (callback) { + if (callback(cur, trail, user_data)) { + goto early_return; + } + } + if (cur && !ht_has(seen, cur)) { + ht_insert(seen, cur, NULL); + queue = refcount_list_push_full(queue, &frame_end_mark, + &ctx->alloc); + obj_held_refs(ctx, cur, &queue); + queue = refcount_list_push_full(queue, cur, &ctx->alloc); + queue = refcount_list_push_full(queue, &frame_start_mark, + &ctx->alloc); + } + } + } + ht_free(seen); + return false; +early_return: + ht_free(seen); + refcount_list_free_full(queue, NULL, &ctx->alloc); + refcount_list_free_full(trail, NULL, &ctx->alloc); + return true; +} /** * Count all instances of a target object by walking the references of some root @@ -859,20 +923,13 @@ bool refcount_context_remove_destructor(const RefcountContext *ctx, void *obj, */ uint64_t refcount_debug_context_count_object(const RefcountContext *ctx, void *obj, void *target) { - static const HTTableFunctions SEEN_FNS = { - .destroy_key = NULL, - .destroy_value = NULL, - .equal = ht_intptr_equal_callback, - .hash = ht_intptr_hash_callback, - .user_data = NULL, - }; if (!obj) { return 0; } RefcountList *queue = NULL; obj_held_refs(ctx, obj, &queue); uint64_t total_count = 0; - HTTable *seen = ht_new(&SEEN_FNS, &ctx->ht_alloc, NULL); + HTTable *seen = ht_new(&DEBUG_SEEN_FNS, &ctx->ht_alloc, NULL); while (queue) { void *cur = queue->data; queue = refcount_list_pop_full(queue, NULL, &ctx->alloc); diff --git a/test/test_refcount.c b/test/test_refcount.c index 382c75b..413aaab 100644 --- a/test/test_refcount.c +++ b/test/test_refcount.c @@ -46,6 +46,16 @@ void reref_destructor(void *a, void *ctx_raw) { refcount_context_ref(ctx, a); } +UNUSED bool print_tree_walk_callback(void *obj, const RefcountList *trail, + void *ignored) { + size_t len = refcount_list_length(trail); + for (size_t i = 0; i < len; ++i) { + fprintf(stderr, " "); + } + fprintf(stderr, "- %p\n", obj); + return false; +} + int main(int argc, const char **argv) { struct ContextAndFlag ctx_and_flag = {.should_be_doing_gc = false}; @@ -153,6 +163,8 @@ int main(int argc, const char **argv) { assert(refcount_context_garbage_collect(c) == 0); assert(refcount_context_unref(c, a_with_destructor)); assert(!refcount_context_is_doing_gc(c)); + /* refcount_debug_context_walk_tree(c, a_with_destructor, */ + /* print_tree_walk_callback, NULL); */ ctx_and_flag.should_be_doing_gc = true; assert(refcount_context_garbage_collect(c) == 26); ctx_and_flag.should_be_doing_gc = false;