mirror of
https://github.com/jemalloc/jemalloc.git
synced 2026-06-02 02:04:20 +03:00
395 lines
13 KiB
C
395 lines
13 KiB
C
#include "test/jemalloc_test.h"
|
|
|
|
#include "jemalloc/internal/pa.h"
|
|
|
|
#define PAC_TEST_ARENA_IND 124
|
|
|
|
static bool pac_test_dalloc_fail;
|
|
static bool pac_test_purge_lazy_fail;
|
|
static unsigned pac_test_alloc_calls;
|
|
static unsigned pac_test_dalloc_calls;
|
|
static unsigned pac_test_destroy_calls;
|
|
static unsigned pac_test_purge_lazy_calls;
|
|
|
|
static void
|
|
pac_test_hooks_reset(void) {
|
|
pac_test_dalloc_fail = false;
|
|
pac_test_purge_lazy_fail = false;
|
|
pac_test_alloc_calls = 0;
|
|
pac_test_dalloc_calls = 0;
|
|
pac_test_destroy_calls = 0;
|
|
pac_test_purge_lazy_calls = 0;
|
|
}
|
|
|
|
static void *
|
|
pac_test_alloc(extent_hooks_t *extent_hooks, void *new_addr, size_t size,
|
|
size_t alignment, bool *zero, bool *commit, unsigned arena_ind) {
|
|
pac_test_alloc_calls++;
|
|
void *ret = pages_map(new_addr, size, alignment, commit);
|
|
return ret;
|
|
}
|
|
|
|
static bool
|
|
pac_test_dalloc(extent_hooks_t *extent_hooks, void *addr, size_t size,
|
|
bool committed, unsigned arena_ind) {
|
|
pac_test_dalloc_calls++;
|
|
if (pac_test_dalloc_fail) {
|
|
return true;
|
|
}
|
|
pages_unmap(addr, size);
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
pac_test_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size,
|
|
bool committed, unsigned arena_ind) {
|
|
pac_test_destroy_calls++;
|
|
pages_unmap(addr, size);
|
|
}
|
|
|
|
static bool
|
|
pac_test_purge_lazy(extent_hooks_t *extent_hooks, void *addr, size_t size,
|
|
size_t offset, size_t length, unsigned arena_ind) {
|
|
pac_test_purge_lazy_calls++;
|
|
return pac_test_purge_lazy_fail;
|
|
}
|
|
|
|
static bool
|
|
pac_test_split(extent_hooks_t *extent_hooks, void *addr, size_t size,
|
|
size_t size_a, size_t size_b, bool committed, unsigned arena_ind) {
|
|
return !maps_coalesce && !opt_retain;
|
|
}
|
|
|
|
static bool
|
|
pac_test_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a,
|
|
void *addr_b, size_t size_b, bool committed, unsigned arena_ind) {
|
|
return !maps_coalesce && !opt_retain;
|
|
}
|
|
|
|
static extent_hooks_t pac_test_hooks = {
|
|
pac_test_alloc,
|
|
pac_test_dalloc,
|
|
pac_test_destroy,
|
|
NULL, /* commit */
|
|
NULL, /* decommit */
|
|
pac_test_purge_lazy,
|
|
NULL, /* purge_forced */
|
|
pac_test_split,
|
|
pac_test_merge
|
|
};
|
|
|
|
typedef struct pac_test_data_s pac_test_data_t;
|
|
struct pac_test_data_s {
|
|
pa_shard_t shard;
|
|
pa_central_t central;
|
|
base_t *base;
|
|
emap_t emap;
|
|
pa_shard_stats_t stats;
|
|
malloc_mutex_t stats_mtx;
|
|
extent_hooks_t hooks;
|
|
};
|
|
|
|
static pac_test_data_t *
|
|
pac_test_data_init(bool custom_hooks, ssize_t dirty_decay_ms,
|
|
ssize_t muzzy_decay_ms) {
|
|
tsdn_t *tsdn = tsdn_fetch();
|
|
pac_test_data_t *data = calloc(1, sizeof(*data));
|
|
assert_ptr_not_null(data, "Unexpected calloc failure");
|
|
|
|
if (custom_hooks) {
|
|
memcpy(&data->hooks, &pac_test_hooks, sizeof(extent_hooks_t));
|
|
} else {
|
|
memcpy(&data->hooks, &ehooks_default_extent_hooks,
|
|
sizeof(extent_hooks_t));
|
|
}
|
|
|
|
data->base = base_new(tsdn, PAC_TEST_ARENA_IND, &data->hooks,
|
|
/* metadata_use_hooks */ true);
|
|
assert_ptr_not_null(data->base, "Unexpected base_new failure");
|
|
assert_false(emap_init(&data->emap, data->base, /* zeroed */ true),
|
|
"Unexpected emap_init failure");
|
|
assert_false(malloc_mutex_init(&data->stats_mtx, "pac_test_stats",
|
|
WITNESS_RANK_ARENA_STATS, malloc_mutex_rank_exclusive),
|
|
"Unexpected stats mutex initialization failure");
|
|
|
|
nstime_t time;
|
|
nstime_init(&time, 0);
|
|
assert_false(pa_central_init(&data->central, data->base, /* hpa */ false,
|
|
&hpa_hooks_default), "Unexpected pa_central_init failure");
|
|
assert_false(pa_shard_init(tsdn, &data->shard, &data->central,
|
|
&data->emap, data->base, PAC_TEST_ARENA_IND, &data->stats,
|
|
&data->stats_mtx, &time, SC_LARGE_MAXCLASS + PAGE,
|
|
dirty_decay_ms, muzzy_decay_ms), "Unexpected pa_shard_init failure");
|
|
|
|
return data;
|
|
}
|
|
|
|
static void
|
|
pac_decay_all_locked(pac_t *pac, extent_state_t state, bool fully_decay) {
|
|
decay_t *decay;
|
|
pac_decay_stats_t *decay_stats;
|
|
ecache_t *ecache;
|
|
if (state == extent_state_dirty) {
|
|
decay = &pac->decay_dirty;
|
|
decay_stats = &pac->stats->decay_dirty;
|
|
ecache = &pac->ecache_dirty;
|
|
} else {
|
|
assert_d_eq(extent_state_muzzy, state,
|
|
"Only dirty and muzzy decay are supported");
|
|
decay = &pac->decay_muzzy;
|
|
decay_stats = &pac->stats->decay_muzzy;
|
|
ecache = &pac->ecache_muzzy;
|
|
}
|
|
|
|
tsdn_t *tsdn = tsdn_fetch();
|
|
malloc_mutex_lock(tsdn, &decay->mtx);
|
|
pac_decay_all(tsdn, pac, decay, decay_stats, ecache, fully_decay);
|
|
malloc_mutex_unlock(tsdn, &decay->mtx);
|
|
}
|
|
|
|
static void
|
|
pac_test_data_destroy(pac_test_data_t *data) {
|
|
/*
|
|
* Decay below calls back into the test hooks; reset all hook state
|
|
* (including the fail flags) so teardown is unaffected by anything the
|
|
* preceding test toggled.
|
|
*/
|
|
pac_test_hooks_reset();
|
|
pac_decay_all_locked(&data->shard.pac, extent_state_dirty,
|
|
/* fully_decay */ true);
|
|
pac_decay_all_locked(&data->shard.pac, extent_state_muzzy,
|
|
/* fully_decay */ true);
|
|
pa_shard_destroy(tsdn_fetch(), &data->shard);
|
|
base_delete(tsdn_fetch(), data->base);
|
|
/*
|
|
* pac operations populated the tsd rtree_ctx with leaf-node pointers
|
|
* from the private emap we just destroyed. Invalidate the cache so
|
|
* the next test's fresh emap doesn't follow stale entries.
|
|
*/
|
|
rtree_ctx_data_init(tsd_rtree_ctx(tsd_fetch()));
|
|
free(data);
|
|
}
|
|
|
|
static edata_t *
|
|
pac_alloc_expect(pac_test_data_t *data, size_t size, bool guarded) {
|
|
bool deferred_work_generated = false;
|
|
edata_t *edata = pac_alloc(tsdn_fetch(), &data->shard.pac, size, PAGE,
|
|
/* zero */ false, guarded, /* frequent_reuse */ false,
|
|
&deferred_work_generated);
|
|
expect_ptr_not_null(edata, "Unexpected pac_alloc failure");
|
|
expect_zu_eq(size, edata_size_get(edata), "Unexpected allocation size");
|
|
return edata;
|
|
}
|
|
|
|
TEST_BEGIN(test_pac_dirty_muzzy_alloc_priority) {
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
pac_t *pac = &data->shard.pac;
|
|
size_t alloc_size = HUGEPAGE;
|
|
size_t alloc_npages = alloc_size >> LG_PAGE;
|
|
|
|
edata_t *muzzy = pac_alloc_expect(
|
|
data, alloc_size, /* guarded */ false);
|
|
void *muzzy_addr = edata_base_get(muzzy);
|
|
edata_t *dirty = pac_alloc_expect(
|
|
data, alloc_size, /* guarded */ false);
|
|
void *dirty_addr = edata_base_get(dirty);
|
|
|
|
bool deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, muzzy, &deferred_work_generated);
|
|
pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ false);
|
|
expect_zu_eq(alloc_npages, ecache_npages_get(&pac->ecache_muzzy),
|
|
"Expected one muzzy page after dirty decay");
|
|
|
|
deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, dirty, &deferred_work_generated);
|
|
expect_zu_eq(alloc_npages, ecache_npages_get(&pac->ecache_dirty),
|
|
"Expected one dirty page");
|
|
|
|
edata_t *from_dirty = pac_alloc_expect(
|
|
data, alloc_size, /* guarded */ false);
|
|
expect_ptr_eq(dirty_addr, edata_base_get(from_dirty),
|
|
"Dirty cache should be preferred over muzzy");
|
|
|
|
edata_t *from_muzzy = pac_alloc_expect(
|
|
data, alloc_size, /* guarded */ false);
|
|
expect_ptr_eq(muzzy_addr, edata_base_get(from_muzzy),
|
|
"Muzzy cache should be used after dirty cache");
|
|
|
|
deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, from_dirty, &deferred_work_generated);
|
|
deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, from_muzzy, &deferred_work_generated);
|
|
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_pac_batched_grow_caches_trailing_dirty) {
|
|
test_skip_if(!sz_large_size_classes_disabled()
|
|
|| !(maps_coalesce || opt_retain));
|
|
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
pac_t *pac = &data->shard.pac;
|
|
|
|
size_t size = HUGEPAGE + PAGE;
|
|
size_t batched_size = sz_s2u_compute_using_delta(size);
|
|
size_t next_hugepage_size = HUGEPAGE_CEILING(size);
|
|
if (batched_size > next_hugepage_size) {
|
|
batched_size = next_hugepage_size;
|
|
}
|
|
assert_zu_gt(batched_size, size,
|
|
"Test size should exercise batched retained growth");
|
|
|
|
size_t dirty_before = ecache_npages_get(&pac->ecache_dirty);
|
|
edata_t *large = pac_alloc_expect(data, size, /* guarded */ false);
|
|
expect_zu_eq(dirty_before + ((batched_size - size) >> LG_PAGE),
|
|
ecache_npages_get(&pac->ecache_dirty),
|
|
"Batched grow should cache the trailing dirty extent");
|
|
if (config_stats) {
|
|
expect_zu_ge(pac_mapped(pac), batched_size,
|
|
"Mapped stats should include the full batched grow");
|
|
}
|
|
|
|
bool deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, large, &deferred_work_generated);
|
|
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_pac_deferred_work_signals) {
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
pac_t *pac = &data->shard.pac;
|
|
|
|
edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
|
|
bool deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
|
|
expect_true(deferred_work_generated,
|
|
"Non-pinned dalloc should request deferred work");
|
|
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_pac_decay_dirty_to_muzzy_via_purge_lazy) {
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
pac_t *pac = &data->shard.pac;
|
|
|
|
edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
|
|
bool deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
|
|
|
|
unsigned purge_lazy_before = pac_test_purge_lazy_calls;
|
|
pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ false);
|
|
expect_zu_eq(0, ecache_npages_get(&pac->ecache_dirty),
|
|
"Dirty decay should remove dirty pages");
|
|
expect_zu_eq(1, ecache_npages_get(&pac->ecache_muzzy),
|
|
"Successful lazy purge should move dirty pages to muzzy");
|
|
expect_u_gt(pac_test_purge_lazy_calls, purge_lazy_before,
|
|
"Dirty-to-muzzy decay should call purge_lazy");
|
|
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_pac_decay_retains_when_dalloc_fails) {
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
pac_t *pac = &data->shard.pac;
|
|
|
|
pac_test_dalloc_fail = true;
|
|
edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
|
|
bool deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
|
|
size_t retained_before = ecache_npages_get(&pac->ecache_retained);
|
|
pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ true);
|
|
expect_zu_gt(ecache_npages_get(&pac->ecache_retained), retained_before,
|
|
"Fully decayed dirty pages should be retained when dalloc fails");
|
|
expect_u_gt(pac_test_dalloc_calls, 0,
|
|
"Fully decayed dirty pages should attempt dalloc first");
|
|
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_pac_non_pinned_dalloc_signals_deferred_work) {
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
pac_t *pac = &data->shard.pac;
|
|
|
|
size_t normal_size = 3 * HUGEPAGE + PAGE;
|
|
edata_t *normal = pac_alloc_expect(data, normal_size,
|
|
/* guarded */ false);
|
|
bool deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), pac, normal, &deferred_work_generated);
|
|
expect_true(deferred_work_generated,
|
|
"Non-pinned dalloc should request deferred work");
|
|
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_pac_non_pinned_shrink_signals_deferred_work) {
|
|
test_skip_if(!maps_coalesce);
|
|
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
pac_t *pac = &data->shard.pac;
|
|
|
|
size_t normal_size = 3 * HUGEPAGE + PAGE;
|
|
size_t normal_shrink_size = 3 * HUGEPAGE;
|
|
edata_t *normal = pac_alloc_expect(data, normal_size,
|
|
/* guarded */ false);
|
|
bool deferred_work_generated = false;
|
|
expect_false(pac_shrink(tsdn_fetch(), pac, normal, normal_size,
|
|
normal_shrink_size, &deferred_work_generated),
|
|
"Unexpected non-pinned shrink failure");
|
|
expect_true(deferred_work_generated,
|
|
"Non-pinned shrink should request deferred work");
|
|
pac_dalloc(tsdn_fetch(), pac, normal, &deferred_work_generated);
|
|
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_pac_large_guarded_dalloc_unguards_before_caching) {
|
|
pac_test_hooks_reset();
|
|
pac_test_data_t *data = pac_test_data_init(
|
|
/* custom_hooks */ true, -1, -1);
|
|
edata_t *guarded = pac_alloc_expect(data, SC_LARGE_MINCLASS,
|
|
/* guarded */ true);
|
|
expect_true(edata_guarded_get(guarded),
|
|
"Guarded allocation should set the guarded bit");
|
|
bool deferred_work_generated = false;
|
|
pac_dalloc(tsdn_fetch(), &data->shard.pac, guarded,
|
|
&deferred_work_generated);
|
|
expect_true(deferred_work_generated,
|
|
"Guarded dalloc should still request deferred work");
|
|
expect_false(edata_guarded_get(guarded),
|
|
"Large guarded dalloc should unguard before caching");
|
|
pac_test_data_destroy(data);
|
|
}
|
|
TEST_END
|
|
|
|
int
|
|
main(void) {
|
|
return test_no_reentrancy(test_pac_dirty_muzzy_alloc_priority,
|
|
test_pac_batched_grow_caches_trailing_dirty,
|
|
test_pac_deferred_work_signals,
|
|
test_pac_decay_dirty_to_muzzy_via_purge_lazy,
|
|
test_pac_decay_retains_when_dalloc_fails,
|
|
test_pac_non_pinned_dalloc_signals_deferred_work,
|
|
test_pac_non_pinned_shrink_signals_deferred_work,
|
|
test_pac_large_guarded_dalloc_unguards_before_caching);
|
|
}
|