From defc7abbb19056e06f89c0f20bab7c8aa89b91d7 Mon Sep 17 00:00:00 2001 From: guangli-dai Date: Tue, 7 Jul 2026 18:08:13 -0700 Subject: [PATCH] Add background-thread state and toggle unit tests New tests cover: - background thread states after arena_reset - background thread stats - toggle background thread on and off during parallel stress allocs/dallocs - background thread fork behavior Add background-thread fork unit test test_fork_background_thread (in fork.c): with the background thread enabled, fork and assert the child comes up with it disabled yet usable (re-enable + alloc round-trips), while the parent keeps its threads. Placed in fork.c to reuse its wait_for_child_exit helper and fork/WIN32 handling instead of duplicating them in a separate file. --- test/unit/background_thread.c | 83 ++++++++++++++++++++++++- test/unit/background_thread_enable.c | 47 ++++++++++++++- test/unit/fork.c | 90 +++++++++++++++++++++++++++- 3 files changed, 216 insertions(+), 4 deletions(-) diff --git a/test/unit/background_thread.c b/test/unit/background_thread.c index 819a81a6..335b7404 100644 --- a/test/unit/background_thread.c +++ b/test/unit/background_thread.c @@ -107,9 +107,88 @@ TEST_BEGIN(test_background_thread_running) { } TEST_END +TEST_BEGIN(test_background_thread_arena_reset) { + test_skip_if(!have_background_thread); + + test_switch_background_thread_ctl(true); + + unsigned arena_ind; + size_t sz = sizeof(arena_ind); + expect_d_eq(mallctl("arenas.create", (void *)&arena_ind, &sz, NULL, 0), + 0, "Unexpected arenas.create failure"); + void *p = mallocx(PAGE, + MALLOCX_TCACHE_NONE | MALLOCX_ARENA(arena_ind)); + expect_ptr_not_null(p, "Unexpected mallocx failure"); + + /* + * Resetting an arena takes its background thread through + * started -> paused -> started (background_thread_arena_reset_begin/ + * finish). The reset frees p, so we must not touch it afterwards. + */ + size_t mib[3]; + size_t miblen = sizeof(mib) / sizeof(mib[0]); + expect_d_eq(mallctlnametomib("arena.0.reset", mib, &miblen), 0, + "Unexpected mallctlnametomib failure"); + mib[1] = arena_ind; + expect_d_eq(mallctlbymib(mib, miblen, NULL, NULL, NULL, 0), 0, + "Unexpected arena reset failure"); + +#if defined(JEMALLOC_BACKGROUND_THREAD) + background_thread_info_t *info = background_thread_info_get(arena_ind); + tsd_t *tsd = tsd_fetch(); + malloc_mutex_lock(tsd_tsdn(tsd), &info->mtx); + background_thread_state_t st = info->state; + malloc_mutex_unlock(tsd_tsdn(tsd), &info->mtx); + expect_d_eq((int)st, (int)background_thread_started, + "Arena reset must leave the background thread started, not paused"); +#endif + expect_zu_gt(n_background_threads, 0, + "Arena reset must not lose background threads"); + + test_switch_background_thread_ctl(false); +} +TEST_END + +TEST_BEGIN(test_background_thread_stats_ctl) { + test_skip_if(!have_background_thread); + test_skip_if(!config_stats); + + test_switch_background_thread_ctl(true); + + uint64_t epoch = 1; + size_t sz = sizeof(epoch); + expect_d_eq(mallctl("epoch", (void *)&epoch, &sz, (void *)&epoch, + sizeof(epoch)), + 0, "Unexpected epoch mallctl failure"); + + size_t num_threads; + sz = sizeof(num_threads); + expect_d_eq(mallctl("stats.background_thread.num_threads", + (void *)&num_threads, &sz, NULL, 0), + 0, "Unexpected stats.background_thread.num_threads failure"); + expect_zu_eq(num_threads, n_background_threads, + "Reported num_threads should match n_background_threads"); + + uint64_t num_runs; + sz = sizeof(num_runs); + expect_d_eq(mallctl("stats.background_thread.num_runs", + (void *)&num_runs, &sz, NULL, 0), + 0, "Unexpected stats.background_thread.num_runs failure"); + + uint64_t run_interval; + sz = sizeof(run_interval); + expect_d_eq(mallctl("stats.background_thread.run_interval", + (void *)&run_interval, &sz, NULL, 0), + 0, "Unexpected stats.background_thread.run_interval failure"); + + test_switch_background_thread_ctl(false); +} +TEST_END + int main(void) { /* Background_thread creation tests reentrancy naturally. */ - return test_no_reentrancy( - test_background_thread_ctl, test_background_thread_running); + return test_no_reentrancy(test_background_thread_ctl, + test_background_thread_running, test_background_thread_arena_reset, + test_background_thread_stats_ctl); } diff --git a/test/unit/background_thread_enable.c b/test/unit/background_thread_enable.c index 57f26c4b..e66bf13c 100644 --- a/test/unit/background_thread_enable.c +++ b/test/unit/background_thread_enable.c @@ -96,7 +96,52 @@ TEST_BEGIN(test_max_background_threads) { } TEST_END +static atomic_b_t stress_stop; + +static void +set_background_thread(bool enable) { + size_t sz = sizeof(bool); + expect_d_eq(mallctl("background_thread", NULL, NULL, &enable, sz), 0, + "Failed to set background_thread"); +} + +static void * +stress_worker(void *arg) { + (void)arg; + while (!atomic_load_b(&stress_stop, ATOMIC_RELAXED)) { + void *p = mallocx(PAGE, MALLOCX_TCACHE_NONE); + if (p != NULL) { + dallocx(p, MALLOCX_TCACHE_NONE); + } + } + return NULL; +} + +TEST_BEGIN(test_toggle_stress_with_concurrent_alloc) { + test_skip_if(!have_background_thread); + + atomic_store_b(&stress_stop, false, ATOMIC_RELAXED); + thd_t thd; + thd_create(&thd, stress_worker, NULL); + + for (unsigned i = 0; i < 200; i++) { + set_background_thread(true); + expect_zu_gt(n_background_threads, 0, + "Background threads should be non-zero after enable " + "(cycle %u)", i); + set_background_thread(false); + expect_zu_eq(n_background_threads, 0, + "Background threads should be zero after disable " + "(cycle %u)", i); + } + + atomic_store_b(&stress_stop, true, ATOMIC_RELAXED); + thd_join(thd, NULL); +} +TEST_END + int main(void) { - return test_no_reentrancy(test_deferred, test_max_background_threads); + return test_no_reentrancy(test_deferred, test_max_background_threads, + test_toggle_stress_with_concurrent_alloc); } diff --git a/test/unit/fork.c b/test/unit/fork.c index ac0b8db7..263fb08a 100644 --- a/test/unit/fork.c +++ b/test/unit/fork.c @@ -568,10 +568,98 @@ TEST_BEGIN(test_fork_postfork_double_fork) { } TEST_END +#ifndef _WIN32 +static bool +get_background_thread_enabled(void) { + bool enabled = false; + size_t sz = sizeof(enabled); + if (mallctl("background_thread", (void *)&enabled, &sz, NULL, 0) != 0) { + return false; + } + return enabled; +} + +static bool +set_background_thread_enabled(bool enabled) { + return mallctl("background_thread", NULL, NULL, &enabled, + sizeof(enabled)) == 0; +} +#endif + +TEST_BEGIN(test_fork_background_thread) { +#ifndef _WIN32 + test_skip_if(!have_background_thread); + + /* Enable the background thread in the parent. */ + bool enabled = true; + expect_d_eq(mallctl("background_thread", NULL, NULL, &enabled, + sizeof(enabled)), + 0, "Unexpected mallctl() failure"); + expect_zu_gt(n_background_threads, 0, + "Number of background threads should be non zero after enabling."); + + pid_t pid = fork(); + if (pid == -1) { + test_fail("Unexpected fork() failure"); + } else if (pid == 0) { + /* + * Child: postfork_child disables the background thread, but the + * allocator must remain usable and re-init capable. + */ + if (get_background_thread_enabled()) { + /* Should report disabled in the child. */ + _exit(1); + } + + /* Several malloc/free round-trips to confirm usability. */ + for (unsigned i = 0; i < 16; i++) { + void *p = mallocx(64, MALLOCX_TCACHE_NONE); + if (p == NULL) { + _exit(2); + } + dallocx(p, MALLOCX_TCACHE_NONE); + } + + void *q = malloc(128); + if (q == NULL) { + _exit(3); + } + free(q); + + /* Re-enable the background thread in the child. */ + if (!set_background_thread_enabled(true)) { + _exit(4); + } + if (!get_background_thread_enabled()) { + _exit(5); + } + + /* And confirm allocation still works afterwards. */ + void *r = mallocx(256, MALLOCX_TCACHE_NONE); + if (r == NULL) { + _exit(6); + } + dallocx(r, MALLOCX_TCACHE_NONE); + + _exit(0); + } else { + /* Parent: keeps its background threads across the fork. */ + wait_for_child_exit(pid); + expect_true(get_background_thread_enabled(), + "Parent should still have background thread enabled."); + expect_zu_gt(n_background_threads, 0, + "Parent should still have background threads after fork."); + } +#else + test_skip("fork(2) is irrelevant to Windows"); +#endif +} +TEST_END + int main(void) { return test_no_reentrancy(test_fork, test_fork_child_usability, test_fork_multithreaded, test_fork_postfork_descriptor_relink, test_fork_postfork_descriptor_relink_multithreaded, - test_fork_postfork_double_fork); + test_fork_postfork_double_fork, test_fork_background_thread); }