mirror of
https://github.com/jemalloc/jemalloc.git
synced 2026-04-14 22:51:50 +03:00
494 lines
14 KiB
C
494 lines
14 KiB
C
#include "test/jemalloc_test.h"
|
|
|
|
#include "jemalloc/internal/sec.h"
|
|
|
|
typedef struct test_data_s test_data_t;
|
|
struct test_data_s {
|
|
/*
|
|
* Must be the first member -- we convert back and forth between the
|
|
* test_data_t and the sec_t;
|
|
*/
|
|
sec_t sec;
|
|
base_t *base;
|
|
};
|
|
|
|
static void
|
|
test_data_init(tsdn_t *tsdn, test_data_t *tdata, const sec_opts_t *opts) {
|
|
tdata->base = base_new(TSDN_NULL, /* ind */ 123,
|
|
&ehooks_default_extent_hooks, /* metadata_use_hooks */ true);
|
|
|
|
bool err = sec_init(tsdn, &tdata->sec, tdata->base, opts);
|
|
assert_false(err, "Unexpected initialization failure");
|
|
if (tdata->sec.opts.nshards > 0) {
|
|
assert_u_ge(tdata->sec.npsizes, 0,
|
|
"Zero size classes allowed for caching");
|
|
}
|
|
}
|
|
|
|
static void
|
|
destroy_test_data(tsdn_t *tsdn, test_data_t *tdata) {
|
|
/* There is no destroy sec to delete the bins ?! */
|
|
base_delete(tsdn, tdata->base);
|
|
}
|
|
|
|
TEST_BEGIN(test_max_nshards_option_zero) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 0;
|
|
opts.max_alloc = PAGE;
|
|
opts.max_bytes = 512 * PAGE;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
edata_t *edata = sec_alloc(tsdn, &tdata.sec, PAGE);
|
|
expect_ptr_null(edata, "SEC should be disabled when nshards==0");
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_max_alloc_option_too_small) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 1;
|
|
opts.max_alloc = 2 * PAGE;
|
|
opts.max_bytes = 512 * PAGE;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
edata_t *edata = sec_alloc(tsdn, &tdata.sec, 3 * PAGE);
|
|
expect_ptr_null(edata, "max_alloc is 2*PAGE, should not alloc 3*PAGE");
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_sec_fill) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 1;
|
|
opts.max_alloc = 2 * PAGE;
|
|
opts.max_bytes = 4 * PAGE;
|
|
opts.batch_fill_extra = 2;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
/* Fill the cache with two extents */
|
|
sec_stats_t stats = {0};
|
|
edata_list_active_t allocs;
|
|
edata_list_active_init(&allocs);
|
|
edata_t edata1, edata2;
|
|
edata_size_set(&edata1, PAGE);
|
|
edata_size_set(&edata2, PAGE);
|
|
edata_list_active_append(&allocs, &edata1);
|
|
edata_list_active_append(&allocs, &edata2);
|
|
sec_fill(tsdn, &tdata.sec, PAGE, &allocs, 2);
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.bytes, 2 * PAGE, "SEC should have what we filled");
|
|
expect_true(edata_list_active_empty(&allocs),
|
|
"extents should be consumed by sec");
|
|
|
|
/* Try to overfill and confirm that max_bytes is respected. */
|
|
stats.bytes = 0;
|
|
edata_t edata5, edata4, edata3;
|
|
edata_size_set(&edata3, PAGE);
|
|
edata_size_set(&edata4, PAGE);
|
|
edata_size_set(&edata5, PAGE);
|
|
edata_list_active_append(&allocs, &edata3);
|
|
edata_list_active_append(&allocs, &edata4);
|
|
edata_list_active_append(&allocs, &edata5);
|
|
sec_fill(tsdn, &tdata.sec, PAGE, &allocs, 3);
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(
|
|
stats.bytes, opts.max_bytes, "SEC can't have more than max_bytes");
|
|
expect_false(edata_list_active_empty(&allocs), "Not all should fit");
|
|
expect_zu_eq(stats.total.noverfills, 1, "Expected one overfill");
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_sec_alloc) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 1;
|
|
opts.max_alloc = 2 * PAGE;
|
|
opts.max_bytes = 4 * PAGE;
|
|
opts.batch_fill_extra = 1;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
/* Alloc from empty cache returns NULL */
|
|
edata_t *edata = sec_alloc(tsdn, &tdata.sec, PAGE);
|
|
expect_ptr_null(edata, "SEC is empty");
|
|
|
|
/* Place two extents into the sec */
|
|
edata_list_active_t allocs;
|
|
edata_list_active_init(&allocs);
|
|
edata_t edata1, edata2;
|
|
edata_size_set(&edata1, PAGE);
|
|
edata_list_active_append(&allocs, &edata1);
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_true(edata_list_active_empty(&allocs), "");
|
|
edata_size_set(&edata2, PAGE);
|
|
edata_list_active_append(&allocs, &edata2);
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_true(edata_list_active_empty(&allocs), "");
|
|
|
|
sec_stats_t stats = {0};
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.bytes, 2 * PAGE,
|
|
"After fill bytes should reflect what is in the cache");
|
|
stats.bytes = 0;
|
|
|
|
/* Most recently cached extent should be used on alloc */
|
|
edata = sec_alloc(tsdn, &tdata.sec, PAGE);
|
|
expect_ptr_eq(edata, &edata2, "edata2 is most recently used");
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.bytes, PAGE, "One more item left in the cache");
|
|
stats.bytes = 0;
|
|
|
|
/* Alloc can still get extents from cache */
|
|
edata = sec_alloc(tsdn, &tdata.sec, PAGE);
|
|
expect_ptr_eq(edata, &edata1, "SEC is not empty");
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.bytes, 0, "No more items after last one is popped");
|
|
|
|
/* And cache is empty again */
|
|
edata = sec_alloc(tsdn, &tdata.sec, PAGE);
|
|
expect_ptr_null(edata, "SEC is empty");
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_sec_dalloc) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 1;
|
|
opts.max_alloc = PAGE;
|
|
opts.max_bytes = 2 * PAGE;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
/* Return one extent into the cache */
|
|
edata_list_active_t allocs;
|
|
edata_list_active_init(&allocs);
|
|
edata_t edata1;
|
|
edata_size_set(&edata1, PAGE);
|
|
edata_list_active_append(&allocs, &edata1);
|
|
|
|
/* SEC is empty, we return one pointer to it */
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_true(
|
|
edata_list_active_empty(&allocs), "extents should be consumed");
|
|
|
|
/* Return one more extent, so that we are at the limit */
|
|
edata_t edata2;
|
|
edata_size_set(&edata2, PAGE);
|
|
edata_list_active_append(&allocs, &edata2);
|
|
/* Sec can take one more as well and we will be exactly at max_bytes */
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_true(
|
|
edata_list_active_empty(&allocs), "extents should be consumed");
|
|
|
|
sec_stats_t stats = {0};
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.bytes, opts.max_bytes, "Size should match deallocs");
|
|
stats.bytes = 0;
|
|
|
|
/*
|
|
* We are at max_bytes. Now, we dalloc one more pointer and we go above
|
|
* the limit. This will force flush to 3/4 of max_bytes and given that
|
|
* we have max of 2 pages, we will have to flush two. We will not flush
|
|
* the one given in the input as it is the most recently used.
|
|
*/
|
|
edata_t edata3;
|
|
edata_size_set(&edata3, PAGE);
|
|
edata_list_active_append(&allocs, &edata3);
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_false(
|
|
edata_list_active_empty(&allocs), "extents should NOT be consumed");
|
|
expect_ptr_ne(
|
|
edata_list_active_first(&allocs), &edata3, "edata3 is MRU");
|
|
expect_ptr_ne(
|
|
edata_list_active_last(&allocs), &edata3, "edata3 is MRU");
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(PAGE, stats.bytes, "Should have flushed");
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_max_bytes_too_low) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 1;
|
|
opts.max_alloc = 4 * PAGE;
|
|
opts.max_bytes = 2 * PAGE;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
/* Return one extent into the cache. Item is too big */
|
|
edata_list_active_t allocs;
|
|
edata_list_active_init(&allocs);
|
|
edata_t edata1;
|
|
edata_size_set(&edata1, 3 * PAGE);
|
|
edata_list_active_append(&allocs, &edata1);
|
|
|
|
/* SEC is empty, we return one pointer to it */
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_false(
|
|
edata_list_active_empty(&allocs), "extents should not be consumed");
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_sec_flush) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 1;
|
|
opts.max_alloc = 4 * PAGE;
|
|
opts.max_bytes = 1024 * PAGE;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
/* We put in 10 one-page extents, and 10 four-page extents */
|
|
edata_list_active_t allocs1;
|
|
edata_list_active_t allocs4;
|
|
edata_list_active_init(&allocs1);
|
|
edata_list_active_init(&allocs4);
|
|
enum { NALLOCS = 10 };
|
|
edata_t edata1[NALLOCS];
|
|
edata_t edata4[NALLOCS];
|
|
for (int i = 0; i < NALLOCS; i++) {
|
|
edata_size_set(&edata1[i], PAGE);
|
|
edata_size_set(&edata4[i], 4 * PAGE);
|
|
|
|
edata_list_active_append(&allocs1, &edata1[i]);
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs1);
|
|
edata_list_active_append(&allocs4, &edata4[i]);
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs4);
|
|
}
|
|
|
|
sec_stats_t stats = {0};
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(
|
|
stats.bytes, 10 * 5 * PAGE, "SEC should have what we filled");
|
|
stats.bytes = 0;
|
|
|
|
expect_true(edata_list_active_empty(&allocs1), "");
|
|
sec_flush(tsdn, &tdata.sec, &allocs1);
|
|
expect_false(edata_list_active_empty(&allocs1), "");
|
|
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.bytes, 0, "SEC should be empty");
|
|
stats.bytes = 0;
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
TEST_BEGIN(test_sec_stats) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
opts.nshards = 1;
|
|
opts.max_alloc = PAGE;
|
|
opts.max_bytes = 2 * PAGE;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
edata_list_active_t allocs;
|
|
edata_list_active_init(&allocs);
|
|
edata_t edata1;
|
|
edata_size_set(&edata1, PAGE);
|
|
edata_list_active_append(&allocs, &edata1);
|
|
|
|
/* SEC is empty alloc fails. nmisses==1 */
|
|
edata_t *edata = sec_alloc(tsdn, &tdata.sec, PAGE);
|
|
expect_ptr_null(edata, "SEC should be empty");
|
|
|
|
/* SEC is empty, we return one pointer to it. ndalloc_noflush=1 */
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_true(
|
|
edata_list_active_empty(&allocs), "extents should be consumed");
|
|
|
|
edata_t edata2;
|
|
edata_size_set(&edata2, PAGE);
|
|
edata_list_active_append(&allocs, &edata2);
|
|
/* Sec can take one more, so ndalloc_noflush=2 */
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_true(
|
|
edata_list_active_empty(&allocs), "extents should be consumed");
|
|
|
|
sec_stats_t stats;
|
|
memset(&stats, 0, sizeof(sec_stats_t));
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.bytes, opts.max_bytes, "Size should match deallocs");
|
|
expect_zu_eq(stats.total.ndalloc_noflush, 2, "");
|
|
expect_zu_eq(stats.total.nmisses, 1, "");
|
|
|
|
memset(&stats, 0, sizeof(sec_stats_t));
|
|
|
|
/*
|
|
* We are at max_bytes. Now, we dalloc one more pointer and we go above
|
|
* the limit. This will force flush, so ndalloc_flush = 1.
|
|
*/
|
|
edata_t edata3;
|
|
edata_size_set(&edata3, PAGE);
|
|
edata_list_active_append(&allocs, &edata3);
|
|
sec_dalloc(tsdn, &tdata.sec, &allocs);
|
|
expect_false(
|
|
edata_list_active_empty(&allocs), "extents should NOT be consumed");
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(PAGE, stats.bytes, "Should have flushed");
|
|
expect_zu_eq(stats.total.ndalloc_flush, 1, "");
|
|
memset(&stats, 0, sizeof(sec_stats_t));
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
#define NOPS_PER_THREAD 100
|
|
#define NPREFILL 32
|
|
|
|
static void
|
|
edata_init_test(edata_t *edata) {
|
|
memset(edata, 0, sizeof(*edata));
|
|
}
|
|
|
|
typedef struct {
|
|
sec_t *sec;
|
|
uint8_t preferred_shard;
|
|
size_t nallocs;
|
|
size_t nallocs_fail;
|
|
size_t ndallocs;
|
|
size_t ndallocs_fail;
|
|
edata_list_active_t fill_list;
|
|
size_t fill_list_sz;
|
|
edata_t *edata[NOPS_PER_THREAD];
|
|
} trylock_test_arg_t;
|
|
|
|
static void *
|
|
thd_trylock_test(void *varg) {
|
|
trylock_test_arg_t *arg = (trylock_test_arg_t *)varg;
|
|
tsd_t *tsd = tsd_fetch();
|
|
tsdn_t *tsdn = tsd_tsdn(tsd);
|
|
|
|
/* Set the preferred shard for this thread */
|
|
uint8_t *shard_idx = tsd_sec_shardp_get(tsd);
|
|
*shard_idx = arg->preferred_shard;
|
|
|
|
/* Fill the shard with some extents */
|
|
sec_fill(tsdn, arg->sec, PAGE, &arg->fill_list, arg->fill_list_sz);
|
|
expect_true(edata_list_active_empty(&arg->fill_list), "");
|
|
|
|
for (unsigned i = 0; i < NOPS_PER_THREAD; i++) {
|
|
/* Try to allocate from SEC */
|
|
arg->edata[i] = sec_alloc(tsdn, arg->sec, PAGE);
|
|
if (arg->edata[i] != NULL) {
|
|
expect_zu_eq(edata_size_get(arg->edata[i]), PAGE, "");
|
|
}
|
|
}
|
|
|
|
for (unsigned i = 0; i < NOPS_PER_THREAD; i++) {
|
|
if (arg->edata[i] != NULL) {
|
|
edata_list_active_t list;
|
|
edata_list_active_init(&list);
|
|
arg->nallocs++;
|
|
edata_list_active_append(&list, arg->edata[i]);
|
|
expect_zu_eq(edata_size_get(arg->edata[i]), PAGE, "");
|
|
sec_dalloc(tsdn, arg->sec, &list);
|
|
if (edata_list_active_empty(&list)) {
|
|
arg->ndallocs++;
|
|
} else {
|
|
arg->ndallocs_fail++;
|
|
}
|
|
} else {
|
|
arg->nallocs_fail++;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
TEST_BEGIN(test_sec_multishard) {
|
|
test_data_t tdata;
|
|
sec_opts_t opts;
|
|
enum { NSHARDS = 2 };
|
|
enum { NTHREADS = NSHARDS * 16 };
|
|
opts.nshards = NSHARDS;
|
|
opts.max_alloc = 2 * PAGE;
|
|
opts.max_bytes = 64 * NTHREADS * PAGE;
|
|
|
|
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
|
|
test_data_init(tsdn, &tdata, &opts);
|
|
|
|
/* Create threads with different preferred shards */
|
|
thd_t thds[NTHREADS];
|
|
trylock_test_arg_t args[NTHREADS];
|
|
|
|
edata_t all_edatas[NPREFILL * NTHREADS];
|
|
|
|
for (unsigned i = 0; i < NTHREADS; i++) {
|
|
edata_list_active_init(&args[i].fill_list);
|
|
for (unsigned j = 0; j < NPREFILL; ++j) {
|
|
size_t ind = i * NPREFILL + j;
|
|
edata_init_test(&all_edatas[ind]);
|
|
edata_size_set(&all_edatas[ind], PAGE);
|
|
edata_list_active_append(
|
|
&args[i].fill_list, &all_edatas[ind]);
|
|
}
|
|
args[i].fill_list_sz = NPREFILL;
|
|
args[i].sec = &tdata.sec;
|
|
args[i].preferred_shard = i % opts.nshards;
|
|
args[i].nallocs = 0;
|
|
args[i].nallocs_fail = 0;
|
|
args[i].ndallocs = 0;
|
|
args[i].ndallocs_fail = 0;
|
|
memset(
|
|
&args[i].edata[0], 0, NOPS_PER_THREAD * sizeof(edata_t *));
|
|
thd_create(&thds[i], thd_trylock_test, &args[i]);
|
|
}
|
|
|
|
for (unsigned i = 0; i < NTHREADS; i++) {
|
|
thd_join(thds[i], NULL);
|
|
}
|
|
|
|
/* Wait for all threads to complete */
|
|
size_t total_allocs = 0;
|
|
size_t total_dallocs = 0;
|
|
size_t total_allocs_fail = 0;
|
|
for (unsigned i = 0; i < NTHREADS; i++) {
|
|
total_allocs += args[i].nallocs;
|
|
total_dallocs += args[i].ndallocs;
|
|
total_allocs_fail += args[i].nallocs_fail;
|
|
}
|
|
|
|
/* We must have at least some hits */
|
|
expect_zu_gt(total_allocs, 0, "");
|
|
/*
|
|
* We must have at least some successful dallocs by design (max_bytes is
|
|
* big enough).
|
|
*/
|
|
expect_zu_gt(total_dallocs, 0, "");
|
|
|
|
/* Get final stats to verify that hits and misses are accurate */
|
|
sec_stats_t stats = {0};
|
|
memset(&stats, 0, sizeof(sec_stats_t));
|
|
sec_stats_merge(tsdn, &tdata.sec, &stats);
|
|
expect_zu_eq(stats.total.nhits, total_allocs, "");
|
|
expect_zu_eq(stats.total.nmisses, total_allocs_fail, "");
|
|
|
|
destroy_test_data(tsdn, &tdata);
|
|
}
|
|
TEST_END
|
|
|
|
int
|
|
main(void) {
|
|
return test(test_max_nshards_option_zero,
|
|
test_max_alloc_option_too_small, test_sec_fill, test_sec_alloc,
|
|
test_sec_dalloc, test_max_bytes_too_low, test_sec_flush,
|
|
test_sec_stats, test_sec_multishard);
|
|
}
|