diff --git a/Makefile.in b/Makefile.in index d6b36971..2a8c5b49 100644 --- a/Makefile.in +++ b/Makefile.in @@ -206,6 +206,7 @@ TESTS_UNIT := \ $(srcroot)test/unit/a0.c \ $(srcroot)test/unit/arena_decay.c \ $(srcroot)test/unit/arena_reset.c \ + $(srcroot)test/unit/arenas_management.c \ $(srcroot)test/unit/atomic.c \ $(srcroot)test/unit/background_thread.c \ $(srcroot)test/unit/background_thread_enable.c \ @@ -247,6 +248,7 @@ TESTS_UNIT := \ $(srcroot)test/unit/hpdata.c \ $(srcroot)test/unit/huge.c \ $(srcroot)test/unit/inspect.c \ + $(srcroot)test/unit/jemalloc_init.c \ $(srcroot)test/unit/junk.c \ $(srcroot)test/unit/junk_alloc.c \ $(srcroot)test/unit/junk_free.c \ diff --git a/test/unit/a0.c b/test/unit/a0.c index 63d792d2..fb05ae5b 100644 --- a/test/unit/a0.c +++ b/test/unit/a0.c @@ -9,7 +9,63 @@ TEST_BEGIN(test_a0) { } TEST_END +TEST_BEGIN(test_a0_multi_sizes) { + static const size_t sizes[] = {1, 8, 1024, 64 * 1024}; + void *ptrs[ARRAY_SIZE(sizes)]; + + for (unsigned i = 0; i < ARRAY_SIZE(sizes); i++) { + ptrs[i] = a0malloc(sizes[i]); + expect_ptr_not_null( + ptrs[i], "Unexpected a0malloc(%zu) failure", sizes[i]); + for (unsigned j = 0; j < i; j++) { + expect_ptr_ne(ptrs[i], ptrs[j], + "a0malloc returned duplicate pointer (i=%u, j=%u)", + i, j); + } + } + for (unsigned i = 0; i < ARRAY_SIZE(sizes); i++) { + a0dalloc(ptrs[i]); + } +} +TEST_END + +TEST_BEGIN(test_a0_ialloc_zero) { + static const size_t sizes[] = {1, 8, 1024, 64 * 1024}; + + for (unsigned i = 0; i < ARRAY_SIZE(sizes); i++) { + size_t size = sizes[i]; + uint8_t *p = (uint8_t *)a0ialloc(size, /* zero */ true, + /* is_internal */ true); + expect_ptr_not_null(p, "Unexpected a0ialloc(zero=true, %zu)", + size); + for (size_t k = 0; k < size; k++) { + expect_u_eq((unsigned)p[k], 0, + "a0ialloc(zero=true) byte %zu of %zu not zero", + k, size); + } + a0idalloc(p, /* is_internal */ true); + } + + /* zero=false: just must not crash and must return non-NULL. */ + void *q = a0ialloc(64, /* zero */ false, /* is_internal */ true); + expect_ptr_not_null(q, "Unexpected a0ialloc(zero=false) failure"); + a0idalloc(q, /* is_internal */ true); +} +TEST_END + +TEST_BEGIN(test_a0_ialloc_internal_flag) { + void *p_internal = a0ialloc(64, false, /* is_internal */ true); + expect_ptr_not_null(p_internal, "a0ialloc(is_internal=true) failed"); + a0idalloc(p_internal, /* is_internal */ true); + + void *p_external = a0ialloc(64, false, /* is_internal */ false); + expect_ptr_not_null(p_external, "a0ialloc(is_internal=false) failed"); + a0idalloc(p_external, /* is_internal */ false); +} +TEST_END + int main(void) { - return test_no_malloc_init(test_a0); + return test_no_malloc_init(test_a0, test_a0_multi_sizes, + test_a0_ialloc_zero, test_a0_ialloc_internal_flag); } diff --git a/test/unit/arenas_management.c b/test/unit/arenas_management.c new file mode 100644 index 00000000..0b42c263 --- /dev/null +++ b/test/unit/arenas_management.c @@ -0,0 +1,254 @@ +#include "test/jemalloc_test.h" +#include "test/arena_util.h" + +#include "jemalloc/internal/arenas_management.h" + +/* + * a0* stuff tested in test/unit/a0, fork related interface in + * test/unit/fork. arena_choose_hard is exercised indirectly on each + * thread's first allocation. + */ + +TEST_BEGIN(test_narenas_total_get_consistent) { + unsigned total = narenas_total_get(); + expect_u_ge(total, narenas_auto, + "narenas_total (%u) must be >= narenas_auto (%u)", total, + narenas_auto); + expect_u_gt(total, 0, "narenas_total must be positive after init"); +} +TEST_END + +TEST_BEGIN(test_narenas_total_set_roundtrip) { + unsigned saved = narenas_total_get(); + narenas_total_set(saved); + expect_u_eq(narenas_total_get(), saved, + "narenas_total_set/get round-trip mismatch"); +} +TEST_END + +TEST_BEGIN(test_narenas_auto_set_roundtrip) { + unsigned saved = narenas_auto; + narenas_auto_set(saved); + expect_u_eq( + narenas_auto, saved, "narenas_auto_set round-trip mismatch"); +} +TEST_END + +TEST_BEGIN(test_manual_arena_base_set_roundtrip) { + unsigned saved = manual_arena_base; + manual_arena_base_set(saved); + expect_u_eq(manual_arena_base, saved, + "manual_arena_base_set round-trip mismatch"); +} +TEST_END + +TEST_BEGIN(test_arena_set_roundtrip) { + tsdn_t *tsdn = tsd_tsdn(tsd_fetch()); + arena_t *a0 = arena_get(tsdn, 0, false); + expect_ptr_not_null(a0, "arena 0 should exist after init"); + + arena_set(0, a0); + expect_ptr_eq(arena_get(tsdn, 0, false), a0, + "arena_set round-trip mismatch for ind=0"); +} +TEST_END + +TEST_BEGIN(test_arena_init_creates_arena) { + tsdn_t *tsdn = tsd_tsdn(tsd_fetch()); + unsigned before = narenas_total_get(); + expect_u_lt(before, MALLOCX_ARENA_LIMIT, + "Cannot create arena: at MALLOCX_ARENA_LIMIT"); + + arena_t *arena = arena_init(tsdn, before, &arena_config_default); + expect_ptr_not_null(arena, "arena_init failed for ind=%u", before); + + expect_u_eq(narenas_total_get(), before + 1, + "narenas_total did not increment after arena_init"); + expect_ptr_eq(arena_get(tsdn, before, false), arena, + "arena_get does not return the freshly-initialized arena"); +} +TEST_END + +TEST_BEGIN(test_arena_init_idempotent_auto) { + test_skip_if(narenas_auto < 1); + + tsdn_t *tsdn = tsd_tsdn(tsd_fetch()); + arena_t *a0 = arena_get(tsdn, 0, false); + expect_ptr_not_null(a0, "arena 0 should exist after init"); + + unsigned before = narenas_total_get(); + arena_t *again = arena_init(tsdn, 0, &arena_config_default); + expect_ptr_eq(again, a0, + "arena_init for an existing auto arena should return same pointer"); + expect_u_eq(narenas_total_get(), before, + "narenas_total should not change for idempotent arena_init"); +} +TEST_END + +/* + * test_arena_migrate spawns one worker thread, binds it to arena1, then has + * it migrate to arena2. We verify the nthreads counters on both arenas and + * that the migrating thread's tsd_arena was re-pointed. + */ +static unsigned migrate_a1_ind; +static unsigned migrate_a2_ind; +static atomic_b_t migrate_done; +static atomic_b_t migrate_go_exit; + +static void * +migrate_worker(void *unused) { + unsigned old_ind; + size_t sz = sizeof(unsigned); + expect_d_eq(mallctl("thread.arena", (void *)&old_ind, &sz, + (void *)&migrate_a1_ind, sizeof(unsigned)), + 0, "thread.arena bind failed"); + + tsd_t *tsd = tsd_fetch(); + tsdn_t *tsdn = tsd_tsdn(tsd); + arena_t *a1 = arena_get(tsdn, migrate_a1_ind, false); + arena_t *a2 = arena_get(tsdn, migrate_a2_ind, false); + expect_ptr_not_null(a1, "arena1 should exist"); + expect_ptr_not_null(a2, "arena2 should exist"); + + arena_migrate(tsd, a1, a2); + + expect_ptr_eq( + tsd_arena_get(tsd), a2, "tsd_arena was not updated to newarena"); + + atomic_store_b(&migrate_done, true, ATOMIC_RELEASE); + + while (!atomic_load_b(&migrate_go_exit, ATOMIC_ACQUIRE)) { + /* Hold the binding so main can read post-migration counts. */ + } + return NULL; +} + +TEST_BEGIN(test_arena_migrate) { + atomic_store_b(&migrate_done, false, ATOMIC_RELEASE); + atomic_store_b(&migrate_go_exit, false, ATOMIC_RELEASE); + + migrate_a1_ind = do_arena_create(-1, -1); + migrate_a2_ind = do_arena_create(-1, -1); + + thd_t thd; + thd_create(&thd, migrate_worker, NULL); + + while (!atomic_load_b(&migrate_done, ATOMIC_ACQUIRE)) { + /* Wait for the migrator to publish results. */ + } + + tsdn_t *tsdn = tsdn_fetch(); + arena_t *a1 = arena_get(tsdn, migrate_a1_ind, false); + arena_t *a2 = arena_get(tsdn, migrate_a2_ind, false); + + expect_u_eq(arena_nthreads_get(a1, false), 0, + "arena1 should have 0 threads after migration"); + expect_u_eq(arena_nthreads_get(a2, false), 1, + "arena2 should have 1 thread after migration"); + + atomic_store_b(&migrate_go_exit, true, ATOMIC_RELEASE); + thd_join(thd, NULL); + + do_arena_destroy(migrate_a1_ind); + do_arena_destroy(migrate_a2_ind); +} +TEST_END + +static atomic_b_t cleanup_skip_worker; + +static void * +arena_cleanup_worker(void *unused) { + tsd_t *tsd = tsd_fetch(); + /* Bind tsd to both an external and internal arena. */ + free(malloc(1)); + + arena_t *a = tsd_arena_get(tsd); + if (a == NULL) { + atomic_store_b(&cleanup_skip_worker, true, ATOMIC_RELEASE); + return NULL; + } + + unsigned pre_nt = arena_nthreads_get(a, false); + + arena_cleanup(tsd); + expect_ptr_null( + tsd_arena_get(tsd), "tsd_arena should be NULL after arena_cleanup"); + expect_u_eq(arena_nthreads_get(a, false), pre_nt - 1, + "external nthreads should decrease by 1 after arena_cleanup"); + + arena_cleanup(tsd); + expect_ptr_null(tsd_arena_get(tsd), + "tsd_arena should remain NULL after second arena_cleanup"); + expect_u_eq(arena_nthreads_get(a, false), pre_nt - 1, + "external nthreads should not change on idempotent arena_cleanup"); + + return NULL; +} + +TEST_BEGIN(test_arena_cleanup) { + atomic_store_b(&cleanup_skip_worker, false, ATOMIC_RELEASE); + + thd_t thd; + thd_create(&thd, arena_cleanup_worker, NULL); + thd_join(thd, NULL); + + if (atomic_load_b(&cleanup_skip_worker, ATOMIC_ACQUIRE)) { + test_skip( + "Worker tsd_arena was NULL after malloc; " + "cannot exercise arena_cleanup path"); + } +} +TEST_END + +static void * +iarena_cleanup_worker(void *unused) { + tsd_t *tsd = tsd_fetch(); + /* Bind tsd to both an external and internal arena. */ + free(malloc(1)); + + arena_t *ia = tsd_iarena_get(tsd); + if (ia == NULL) { + atomic_store_b(&cleanup_skip_worker, true, ATOMIC_RELEASE); + return NULL; + } + + unsigned pre_nt = arena_nthreads_get(ia, true); + + iarena_cleanup(tsd); + expect_ptr_null(tsd_iarena_get(tsd), + "tsd_iarena should be NULL after iarena_cleanup"); + expect_u_eq(arena_nthreads_get(ia, true), pre_nt - 1, + "internal nthreads should decrease by 1 after iarena_cleanup"); + + iarena_cleanup(tsd); + expect_ptr_null(tsd_iarena_get(tsd), + "tsd_iarena should remain NULL after second iarena_cleanup"); + expect_u_eq(arena_nthreads_get(ia, true), pre_nt - 1, + "internal nthreads should not change on idempotent iarena_cleanup"); + + return NULL; +} + +TEST_BEGIN(test_iarena_cleanup) { + atomic_store_b(&cleanup_skip_worker, false, ATOMIC_RELEASE); + + thd_t thd; + thd_create(&thd, iarena_cleanup_worker, NULL); + thd_join(thd, NULL); + + if (atomic_load_b(&cleanup_skip_worker, ATOMIC_ACQUIRE)) { + test_skip( + "Worker tsd_iarena was NULL after malloc; " + "cannot exercise iarena_cleanup path"); + } +} +TEST_END + +int +main(void) { + return test(test_narenas_total_get_consistent, + test_narenas_total_set_roundtrip, test_narenas_auto_set_roundtrip, + test_manual_arena_base_set_roundtrip, test_arena_set_roundtrip, + test_arena_init_creates_arena, test_arena_init_idempotent_auto, + test_arena_migrate, test_arena_cleanup, test_iarena_cleanup); +} diff --git a/test/unit/fork.c b/test/unit/fork.c index 60675b77..fb0528a1 100644 --- a/test/unit/fork.c +++ b/test/unit/fork.c @@ -105,6 +105,84 @@ do_test_fork_multithreaded(void) { } #endif +TEST_BEGIN(test_fork_child_usability) { +#ifndef _WIN32 + void *p; + pid_t pid; + unsigned arena_ind; + size_t sz = sizeof(unsigned); + + expect_d_eq(mallctl("arenas.create", (void *)&arena_ind, &sz, NULL, 0), + 0, "Unexpected mallctl() failure"); + + p = mallocx(1, MALLOCX_ARENA(arena_ind) | MALLOCX_TCACHE_NONE); + expect_ptr_not_null(p, "Unexpected mallocx() failure"); + + pid = fork(); + + if (pid == -1) { + test_fail("Unexpected fork() failure"); + } else if (pid == 0) { + /* Child: exercise non-trivial allocator operations. */ + + /* Free pre-fork pointer (arena mutexes must be unlocked). */ + dallocx(p, MALLOCX_TCACHE_NONE); + + /* Basic malloc/free. */ + void *q = malloc(64); + if (q == NULL) { + _exit(1); + } + free(q); + + /* Create a new arena (ctl_mtx + arenas_lock re-init). */ + unsigned child_arena; + size_t csz = sizeof(unsigned); + if (mallctl("arenas.create", (void *)&child_arena, &csz, + NULL, 0) != 0) { + _exit(2); + } + + /* Allocate from the new arena. */ + q = mallocx(128, + MALLOCX_ARENA(child_arena) | MALLOCX_TCACHE_NONE); + if (q == NULL) { + _exit(3); + } + dallocx(q, MALLOCX_TCACHE_NONE); + + /* Read narenas (ctl read path). */ + unsigned narenas; + csz = sizeof(unsigned); + if (mallctl("arenas.narenas", (void *)&narenas, &csz, + NULL, 0) != 0) { + _exit(4); + } + + /* Destroy the child-created arena. */ + size_t mib[3]; + size_t miblen = ARRAY_SIZE(mib); + if (mallctlnametomib("arena.0.destroy", mib, &miblen) + != 0) { + _exit(5); + } + mib[1] = (size_t)child_arena; + if (mallctlbymib(mib, miblen, NULL, NULL, NULL, 0) != 0) { + _exit(6); + } + + _exit(0); + } else { + /* Parent. */ + dallocx(p, MALLOCX_TCACHE_NONE); + wait_for_child_exit(pid); + } +#else + test_skip("fork(2) is irrelevant to Windows"); +#endif +} +TEST_END + TEST_BEGIN(test_fork_multithreaded) { #ifndef _WIN32 /* @@ -137,5 +215,6 @@ TEST_END int main(void) { - return test_no_reentrancy(test_fork, test_fork_multithreaded); + return test_no_reentrancy(test_fork, test_fork_child_usability, + test_fork_multithreaded); } diff --git a/test/unit/jemalloc_init.c b/test/unit/jemalloc_init.c new file mode 100644 index 00000000..f8da3174 --- /dev/null +++ b/test/unit/jemalloc_init.c @@ -0,0 +1,74 @@ +#include "test/jemalloc_test.h" + +#include "jemalloc/internal/jemalloc_init.h" + +TEST_BEGIN(test_malloc_init_hard_idempotent) { + expect_false(malloc_init_hard(), + "malloc_init_hard should return false when already initialized"); + expect_true(malloc_initialized(), + "malloc_initialized should still be true after re-calling " + "malloc_init_hard"); +} +TEST_END + +TEST_BEGIN(test_malloc_initializer_set_idempotent_on_main) { + test_skip_if(!malloc_is_initializer()); + + malloc_initializer_set(); + + expect_true(malloc_is_initializer(), + "malloc_is_initializer should still be true after re-setting " + "from the same thread"); + expect_true(malloc_initializer_is_set(), + "malloc_initializer_is_set should still be true after re-setting"); +} +TEST_END + +#ifdef JEMALLOC_THREADED_INIT +static atomic_b_t initializer_worker_done; +static atomic_b_t initializer_worker_result; + +static void * +is_initializer_worker(void *unused) { + atomic_store_b(&initializer_worker_result, malloc_is_initializer(), + ATOMIC_RELEASE); + atomic_store_b(&initializer_worker_done, true, ATOMIC_RELEASE); + return NULL; +} +#endif + +TEST_BEGIN(test_malloc_is_initializer_false_in_worker) { +#ifndef JEMALLOC_THREADED_INIT + /* + * Without JEMALLOC_THREADED_INIT (e.g. macOS), malloc_initializer + * is a process-wide bool and malloc_is_initializer() returns true + * for every thread after init. The per-thread distinction this + * test exercises only exists in threaded-init builds. + */ + test_skip_if(true); +#else + test_skip_if(!malloc_is_initializer()); + + atomic_store_b(&initializer_worker_done, false, ATOMIC_RELEASE); + atomic_store_b(&initializer_worker_result, false, ATOMIC_RELEASE); + + thd_t thd; + thd_create(&thd, is_initializer_worker, NULL); + thd_join(thd, NULL); + + expect_true( + atomic_load_b(&initializer_worker_done, ATOMIC_ACQUIRE), + "Worker should have completed"); + expect_false( + atomic_load_b(&initializer_worker_result, ATOMIC_ACQUIRE), + "malloc_is_initializer should be false in a non-init thread"); +#endif +} +TEST_END + +int +main(void) { + return test(test_malloc_init_hard_idempotent, + test_malloc_initializer_set_idempotent_on_main, + test_malloc_is_initializer_false_in_worker); +}