Home | History | Annotate | Line # | Download | only in unit
      1 #include "test/jemalloc_test.h"
      2 
      3 #include "jemalloc/internal/pa.h"
      4 
      5 /*
      6  * pac_decay_deferred is JET_EXTERN (test-only; see src/pac.c) and has no public
      7  * header declaration, so declare it locally for the deterministic test below.
      8  */
      9 extern void pac_decay_deferred(tsdn_t *tsdn, pac_t *pac,
     10     pac_purge_eagerness_t eagerness, pac_deferred_work_result_t *result);
     11 
     12 #define PAC_TEST_ARENA_IND 124
     13 
     14 static bool pac_test_dalloc_fail;
     15 static bool pac_test_purge_lazy_fail;
     16 static unsigned pac_test_alloc_calls;
     17 static unsigned pac_test_dalloc_calls;
     18 static unsigned pac_test_destroy_calls;
     19 static unsigned pac_test_purge_lazy_calls;
     20 
     21 static void
     22 pac_test_hooks_reset(void) {
     23 	pac_test_dalloc_fail = false;
     24 	pac_test_purge_lazy_fail = false;
     25 	pac_test_alloc_calls = 0;
     26 	pac_test_dalloc_calls = 0;
     27 	pac_test_destroy_calls = 0;
     28 	pac_test_purge_lazy_calls = 0;
     29 }
     30 
     31 static void *
     32 pac_test_alloc(extent_hooks_t *extent_hooks, void *new_addr, size_t size,
     33     size_t alignment, bool *zero, bool *commit, unsigned arena_ind) {
     34 	pac_test_alloc_calls++;
     35 	void *ret = pages_map(new_addr, size, alignment, commit);
     36 	return ret;
     37 }
     38 
     39 static bool
     40 pac_test_dalloc(extent_hooks_t *extent_hooks, void *addr, size_t size,
     41     bool committed, unsigned arena_ind) {
     42 	pac_test_dalloc_calls++;
     43 	if (pac_test_dalloc_fail) {
     44 		return true;
     45 	}
     46 	pages_unmap(addr, size);
     47 	return false;
     48 }
     49 
     50 static void
     51 pac_test_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size,
     52     bool committed, unsigned arena_ind) {
     53 	pac_test_destroy_calls++;
     54 	pages_unmap(addr, size);
     55 }
     56 
     57 static bool
     58 pac_test_purge_lazy(extent_hooks_t *extent_hooks, void *addr, size_t size,
     59     size_t offset, size_t length, unsigned arena_ind) {
     60 	pac_test_purge_lazy_calls++;
     61 	return pac_test_purge_lazy_fail;
     62 }
     63 
     64 static bool
     65 pac_test_split(extent_hooks_t *extent_hooks, void *addr, size_t size,
     66     size_t size_a, size_t size_b, bool committed, unsigned arena_ind) {
     67 	return !maps_coalesce && !opt_retain;
     68 }
     69 
     70 static bool
     71 pac_test_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a,
     72     void *addr_b, size_t size_b, bool committed, unsigned arena_ind) {
     73 	return !maps_coalesce && !opt_retain;
     74 }
     75 
     76 static extent_hooks_t pac_test_hooks = {
     77 	pac_test_alloc,
     78 	pac_test_dalloc,
     79 	pac_test_destroy,
     80 	NULL, /* commit */
     81 	NULL, /* decommit */
     82 	pac_test_purge_lazy,
     83 	NULL, /* purge_forced */
     84 	pac_test_split,
     85 	pac_test_merge
     86 };
     87 
     88 typedef struct pac_test_data_s pac_test_data_t;
     89 struct pac_test_data_s {
     90 	pa_shard_t shard;
     91 	pa_central_t central;
     92 	base_t *base;
     93 	emap_t emap;
     94 	pa_shard_stats_t stats;
     95 	malloc_mutex_t stats_mtx;
     96 	extent_hooks_t hooks;
     97 };
     98 
     99 static pac_test_data_t *
    100 pac_test_data_init(bool custom_hooks, ssize_t dirty_decay_ms,
    101     ssize_t muzzy_decay_ms) {
    102 	tsdn_t *tsdn = tsdn_fetch();
    103 	pac_test_data_t *data = calloc(1, sizeof(*data));
    104 	assert_ptr_not_null(data, "Unexpected calloc failure");
    105 
    106 	if (custom_hooks) {
    107 		memcpy(&data->hooks, &pac_test_hooks, sizeof(extent_hooks_t));
    108 	} else {
    109 		memcpy(&data->hooks, &ehooks_default_extent_hooks,
    110 		    sizeof(extent_hooks_t));
    111 	}
    112 
    113 	data->base = base_new(tsdn, PAC_TEST_ARENA_IND, &data->hooks,
    114 	    /* metadata_use_hooks */ true);
    115 	assert_ptr_not_null(data->base, "Unexpected base_new failure");
    116 	assert_false(emap_init(&data->emap, data->base, /* zeroed */ true),
    117 	    "Unexpected emap_init failure");
    118 	assert_false(malloc_mutex_init(&data->stats_mtx, "pac_test_stats",
    119 	    WITNESS_RANK_ARENA_STATS, malloc_mutex_rank_exclusive),
    120 	    "Unexpected stats mutex initialization failure");
    121 
    122 	nstime_t time;
    123 	nstime_init(&time, 0);
    124 	assert_false(pa_central_init(&data->central, data->base,
    125 	    /* hpa */ false, &hpa_hooks_default),
    126 	    "Unexpected pa_central_init failure");
    127 	assert_false(pa_shard_init(tsdn, &data->shard, &data->central,
    128 	    &data->emap, data->base, PAC_TEST_ARENA_IND, &data->stats,
    129 	    &data->stats_mtx, &time, SC_LARGE_MAXCLASS + PAGE,
    130 	    dirty_decay_ms, muzzy_decay_ms),
    131 	    "Unexpected pa_shard_init failure");
    132 
    133 	return data;
    134 }
    135 
    136 static void
    137 pac_decay_all_locked(pac_t *pac, extent_state_t state, bool fully_decay) {
    138 	decay_t *decay;
    139 	pac_decay_stats_t *decay_stats;
    140 	ecache_t *ecache;
    141 	if (state == extent_state_dirty) {
    142 		decay = &pac->decay_dirty;
    143 		decay_stats = &pac->stats->decay_dirty;
    144 		ecache = &pac->ecache_dirty;
    145 	} else {
    146 		assert_d_eq(extent_state_muzzy, state,
    147 		    "Only dirty and muzzy decay are supported");
    148 		decay = &pac->decay_muzzy;
    149 		decay_stats = &pac->stats->decay_muzzy;
    150 		ecache = &pac->ecache_muzzy;
    151 	}
    152 
    153 	tsdn_t *tsdn = tsdn_fetch();
    154 	malloc_mutex_lock(tsdn, &decay->mtx);
    155 	pac_decay_all(tsdn, pac, decay, decay_stats, ecache, fully_decay);
    156 	malloc_mutex_unlock(tsdn, &decay->mtx);
    157 }
    158 
    159 static void
    160 pac_test_data_destroy(pac_test_data_t *data) {
    161 	/*
    162 	 * Decay below calls back into the test hooks; reset all hook state
    163 	 * (including the fail flags) so teardown is unaffected by anything the
    164 	 * preceding test toggled.
    165 	 */
    166 	pac_test_hooks_reset();
    167 	pac_decay_all_locked(&data->shard.pac, extent_state_dirty,
    168 	    /* fully_decay */ true);
    169 	pac_decay_all_locked(&data->shard.pac, extent_state_muzzy,
    170 	    /* fully_decay */ true);
    171 	pa_shard_destroy(tsdn_fetch(), &data->shard);
    172 	base_delete(tsdn_fetch(), data->base);
    173 	/*
    174 	 * pac operations populated the tsd rtree_ctx with leaf-node pointers
    175 	 * from the private emap we just destroyed.  Invalidate the cache so
    176 	 * the next test's fresh emap doesn't follow stale entries.
    177 	 */
    178 	rtree_ctx_data_init(tsd_rtree_ctx(tsd_fetch()));
    179 	free(data);
    180 }
    181 
    182 static edata_t *
    183 pac_alloc_expect(pac_test_data_t *data, size_t size, bool guarded) {
    184 	bool deferred_work_generated = false;
    185 	edata_t *edata = pac_alloc(tsdn_fetch(), &data->shard.pac, size, PAGE,
    186 	    /* zero */ false, guarded, /* frequent_reuse */ false,
    187 	    &deferred_work_generated);
    188 	expect_ptr_not_null(edata, "Unexpected pac_alloc failure");
    189 	expect_zu_eq(size, edata_size_get(edata), "Unexpected allocation size");
    190 	return edata;
    191 }
    192 
    193 TEST_BEGIN(test_pac_dirty_muzzy_alloc_priority) {
    194 	pac_test_hooks_reset();
    195 	pac_test_data_t *data = pac_test_data_init(
    196 	    /* custom_hooks */ true, -1, -1);
    197 	pac_t *pac = &data->shard.pac;
    198 	size_t alloc_size = HUGEPAGE;
    199 	size_t alloc_npages = alloc_size >> LG_PAGE;
    200 
    201 	edata_t *muzzy = pac_alloc_expect(
    202 	    data, alloc_size, /* guarded */ false);
    203 	void *muzzy_addr = edata_base_get(muzzy);
    204 	edata_t *dirty = pac_alloc_expect(
    205 	    data, alloc_size, /* guarded */ false);
    206 	void *dirty_addr = edata_base_get(dirty);
    207 
    208 	bool deferred_work_generated = false;
    209 	pac_dalloc(tsdn_fetch(), pac, muzzy, &deferred_work_generated);
    210 	pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ false);
    211 	expect_zu_eq(alloc_npages, ecache_npages_get(&pac->ecache_muzzy),
    212 	    "Expected one muzzy page after dirty decay");
    213 
    214 	deferred_work_generated = false;
    215 	pac_dalloc(tsdn_fetch(), pac, dirty, &deferred_work_generated);
    216 	expect_zu_eq(alloc_npages, ecache_npages_get(&pac->ecache_dirty),
    217 	    "Expected one dirty page");
    218 
    219 	edata_t *from_dirty = pac_alloc_expect(
    220 	    data, alloc_size, /* guarded */ false);
    221 	expect_ptr_eq(dirty_addr, edata_base_get(from_dirty),
    222 	    "Dirty cache should be preferred over muzzy");
    223 
    224 	edata_t *from_muzzy = pac_alloc_expect(
    225 	    data, alloc_size, /* guarded */ false);
    226 	expect_ptr_eq(muzzy_addr, edata_base_get(from_muzzy),
    227 	    "Muzzy cache should be used after dirty cache");
    228 
    229 	deferred_work_generated = false;
    230 	pac_dalloc(tsdn_fetch(), pac, from_dirty, &deferred_work_generated);
    231 	deferred_work_generated = false;
    232 	pac_dalloc(tsdn_fetch(), pac, from_muzzy, &deferred_work_generated);
    233 
    234 	pac_test_data_destroy(data);
    235 }
    236 TEST_END
    237 
    238 TEST_BEGIN(test_pac_batched_grow_caches_trailing_dirty) {
    239 	test_skip_if(!sz_large_size_classes_disabled()
    240 	    || !(maps_coalesce || opt_retain));
    241 
    242 	pac_test_hooks_reset();
    243 	pac_test_data_t *data = pac_test_data_init(
    244 	    /* custom_hooks */ true, -1, -1);
    245 	pac_t *pac = &data->shard.pac;
    246 
    247 	size_t size = HUGEPAGE + PAGE;
    248 	size_t batched_size = sz_s2u_compute_using_delta(size);
    249 	size_t next_hugepage_size = HUGEPAGE_CEILING(size);
    250 	if (batched_size > next_hugepage_size) {
    251 		batched_size = next_hugepage_size;
    252 	}
    253 	assert_zu_gt(batched_size, size,
    254 	    "Test size should exercise batched retained growth");
    255 
    256 	size_t dirty_before = ecache_npages_get(&pac->ecache_dirty);
    257 	edata_t *large = pac_alloc_expect(data, size, /* guarded */ false);
    258 	expect_zu_eq(dirty_before + ((batched_size - size) >> LG_PAGE),
    259 	    ecache_npages_get(&pac->ecache_dirty),
    260 	    "Batched grow should cache the trailing dirty extent");
    261 	if (config_stats) {
    262 		expect_zu_ge(pac_mapped(pac), batched_size,
    263 		    "Mapped stats should include the full batched grow");
    264 	}
    265 
    266 	bool deferred_work_generated = false;
    267 	pac_dalloc(tsdn_fetch(), pac, large, &deferred_work_generated);
    268 
    269 	pac_test_data_destroy(data);
    270 }
    271 TEST_END
    272 
    273 TEST_BEGIN(test_pac_deferred_work_signals) {
    274 	pac_test_hooks_reset();
    275 	pac_test_data_t *data = pac_test_data_init(
    276 	    /* custom_hooks */ true, -1, -1);
    277 	pac_t *pac = &data->shard.pac;
    278 
    279 	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
    280 	bool deferred_work_generated = false;
    281 	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
    282 	expect_true(deferred_work_generated,
    283 	    "Non-pinned dalloc should request deferred work");
    284 
    285 	pac_test_data_destroy(data);
    286 }
    287 TEST_END
    288 
    289 TEST_BEGIN(test_pac_decay_dirty_to_muzzy_via_purge_lazy) {
    290 	pac_test_hooks_reset();
    291 	pac_test_data_t *data = pac_test_data_init(
    292 	    /* custom_hooks */ true, -1, -1);
    293 	pac_t *pac = &data->shard.pac;
    294 
    295 	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
    296 	bool deferred_work_generated = false;
    297 	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
    298 
    299 	unsigned purge_lazy_before = pac_test_purge_lazy_calls;
    300 	pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ false);
    301 	expect_zu_eq(0, ecache_npages_get(&pac->ecache_dirty),
    302 	    "Dirty decay should remove dirty pages");
    303 	expect_zu_eq(1, ecache_npages_get(&pac->ecache_muzzy),
    304 	    "Successful lazy purge should move dirty pages to muzzy");
    305 	expect_u_gt(pac_test_purge_lazy_calls, purge_lazy_before,
    306 	    "Dirty-to-muzzy decay should call purge_lazy");
    307 
    308 	pac_test_data_destroy(data);
    309 }
    310 TEST_END
    311 
    312 TEST_BEGIN(test_pac_decay_retains_when_dalloc_fails) {
    313 	pac_test_hooks_reset();
    314 	pac_test_data_t *data = pac_test_data_init(
    315 	    /* custom_hooks */ true, -1, -1);
    316 	pac_t *pac = &data->shard.pac;
    317 
    318 	pac_test_dalloc_fail = true;
    319 	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
    320 	bool deferred_work_generated = false;
    321 	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
    322 	size_t retained_before = ecache_npages_get(&pac->ecache_retained);
    323 	pac_decay_all_locked(pac, extent_state_dirty, /* fully_decay */ true);
    324 	expect_zu_gt(ecache_npages_get(&pac->ecache_retained), retained_before,
    325 	    "Fully decayed dirty pages should be retained when dalloc fails");
    326 	expect_u_gt(pac_test_dalloc_calls, 0,
    327 	    "Fully decayed dirty pages should attempt dalloc first");
    328 
    329 	pac_test_data_destroy(data);
    330 }
    331 TEST_END
    332 
    333 TEST_BEGIN(test_pac_non_pinned_dalloc_signals_deferred_work) {
    334 	pac_test_hooks_reset();
    335 	pac_test_data_t *data = pac_test_data_init(
    336 	    /* custom_hooks */ true, -1, -1);
    337 	pac_t *pac = &data->shard.pac;
    338 
    339 	size_t normal_size = 3 * HUGEPAGE + PAGE;
    340 	edata_t *normal = pac_alloc_expect(data, normal_size,
    341 	    /* guarded */ false);
    342 	bool deferred_work_generated = false;
    343 	pac_dalloc(tsdn_fetch(), pac, normal, &deferred_work_generated);
    344 	expect_true(deferred_work_generated,
    345 	    "Non-pinned dalloc should request deferred work");
    346 
    347 	pac_test_data_destroy(data);
    348 }
    349 TEST_END
    350 
    351 TEST_BEGIN(test_pac_non_pinned_shrink_signals_deferred_work) {
    352 	test_skip_if(!maps_coalesce);
    353 
    354 	pac_test_hooks_reset();
    355 	pac_test_data_t *data = pac_test_data_init(
    356 	    /* custom_hooks */ true, -1, -1);
    357 	pac_t *pac = &data->shard.pac;
    358 
    359 	size_t normal_size = 3 * HUGEPAGE + PAGE;
    360 	size_t normal_shrink_size = 3 * HUGEPAGE;
    361 	edata_t *normal = pac_alloc_expect(data, normal_size,
    362 	    /* guarded */ false);
    363 	bool deferred_work_generated = false;
    364 	expect_false(pac_shrink(tsdn_fetch(), pac, normal, normal_size,
    365 	    normal_shrink_size, &deferred_work_generated),
    366 	    "Unexpected non-pinned shrink failure");
    367 	expect_true(deferred_work_generated,
    368 	    "Non-pinned shrink should request deferred work");
    369 	pac_dalloc(tsdn_fetch(), pac, normal, &deferred_work_generated);
    370 
    371 	pac_test_data_destroy(data);
    372 }
    373 TEST_END
    374 
    375 TEST_BEGIN(test_pac_large_guarded_dalloc_unguards_before_caching) {
    376 	pac_test_hooks_reset();
    377 	pac_test_data_t *data = pac_test_data_init(
    378 	    /* custom_hooks */ true, -1, -1);
    379 	edata_t *guarded = pac_alloc_expect(data, SC_LARGE_MINCLASS,
    380 	    /* guarded */ true);
    381 	expect_true(edata_guarded_get(guarded),
    382 	    "Guarded allocation should set the guarded bit");
    383 	bool deferred_work_generated = false;
    384 	pac_dalloc(tsdn_fetch(), &data->shard.pac, guarded,
    385 	    &deferred_work_generated);
    386 	expect_true(deferred_work_generated,
    387 	    "Guarded dalloc should still request deferred work");
    388 	expect_false(edata_guarded_get(guarded),
    389 	    "Large guarded dalloc should unguard before caching");
    390 	pac_test_data_destroy(data);
    391 }
    392 TEST_END
    393 
    394 TEST_BEGIN(test_pac_deferred_never_does_not_purge) {
    395 	/*
    396 	 * PAC_PURGE_NEVER is the eagerness the application free path selects
    397 	 * when a background thread is enabled, meaning current thread does not
    398 	 * purge.  It is only reachable through the exposed pac_decay_deferred
    399 	 * primitive (the production entry pac_do_deferred_work picks eagerness
    400 	 * itself), so this is a deliberate internal test of that critical,
    401 	 * stable edge.
    402 	 */
    403 	pac_test_hooks_reset();
    404 	/*
    405 	 * Finite dirty decay so a purge is gated by eagerness, not decay_ms<=0.
    406 	 */
    407 	pac_test_data_t *data = pac_test_data_init(
    408 	    /* custom_hooks */ true, /* dirty_decay_ms */ 100 * 1000, -1);
    409 	pac_t *pac = &data->shard.pac;
    410 
    411 	edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
    412 	size_t ndirty_before_dalloc = ecache_npages_get(&pac->ecache_dirty);
    413 
    414 	/* Dalloc caches the extent as dirty and requests deferred work. */
    415 	bool deferred_work_generated = false;
    416 	pac_dalloc(tsdn_fetch(), pac, edata, &deferred_work_generated);
    417 	expect_true(deferred_work_generated,
    418 	    "Non-pinned dalloc should request deferred work");
    419 	size_t ndirty_after_dalloc = ecache_npages_get(&pac->ecache_dirty);
    420 	expect_zu_gt(ndirty_after_dalloc, ndirty_before_dalloc,
    421 	    "Dalloc should add dirty pages");
    422 
    423 	/* PAC_PURGE_NEVER must leave the dirty pages untouched. */
    424 	pac_deferred_work_result_t result;
    425 	pac_decay_deferred(tsdn_fetch(), pac, PAC_PURGE_NEVER, &result);
    426 	expect_zu_eq(ecache_npages_get(&pac->ecache_dirty), ndirty_after_dalloc,
    427 	    "PAC_PURGE_NEVER must not purge any dirty pages");
    428 
    429 	pac_test_data_destroy(data);
    430 }
    431 TEST_END
    432 
    433 int
    434 main(void) {
    435 	return test_no_reentrancy(test_pac_dirty_muzzy_alloc_priority,
    436 	    test_pac_batched_grow_caches_trailing_dirty,
    437 	    test_pac_deferred_work_signals,
    438 	    test_pac_decay_dirty_to_muzzy_via_purge_lazy,
    439 	    test_pac_decay_retains_when_dalloc_fails,
    440 	    test_pac_non_pinned_dalloc_signals_deferred_work,
    441 	    test_pac_non_pinned_shrink_signals_deferred_work,
    442 	    test_pac_large_guarded_dalloc_unguards_before_caching,
    443 	    test_pac_deferred_never_does_not_purge);
    444 }
    445