mirror of
https://github.com/jemalloc/jemalloc.git
synced 2026-07-16 13:47:28 +03:00
Improve unit test coverage for jemalloc_init, arenas_management, and jemalloc_fork modules
This commit is contained in:
parent
ba1e2fe4db
commit
6e0b8e6daa
5 changed files with 467 additions and 2 deletions
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
254
test/unit/arenas_management.c
Normal file
254
test/unit/arenas_management.c
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
74
test/unit/jemalloc_init.c
Normal file
74
test/unit/jemalloc_init.c
Normal file
|
|
@ -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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue