Add PAC decay/deferral unit test coverage.

Add unit tests to cover public interface of pac decay & deferral
relevant functions.  To align with the decopuling between arena and pac,
rename arena_decay to pac_decay here.
This commit is contained in:
guangli-dai 2026-06-28 15:29:38 -07:00
parent 3688de0623
commit 11f6e4ee91
5 changed files with 144 additions and 8 deletions

View file

@ -204,7 +204,6 @@ C_UTIL_CPP_SRCS := $(srcroot)src/nstime.c $(srcroot)src/malloc_io.c
endif
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 \
@ -272,6 +271,7 @@ TESTS_UNIT := \
$(srcroot)test/unit/oversize_threshold.c \
$(srcroot)test/unit/pa.c \
$(srcroot)test/unit/pac.c \
$(srcroot)test/unit/pac_decay.c \
$(srcroot)test/unit/pac_sec_integration.c \
$(srcroot)test/unit/pack.c \
$(srcroot)test/unit/pages.c \

View file

@ -273,9 +273,67 @@ TEST_BEGIN(test_decay_ns_until_purge) {
}
TEST_END
/*
* Branch coverage for the decay-level math the early-wake path
* (pac_decay_should_wake_early) relies on, exercised here at the public decay
* seam with explicit nstime rather than via real-thread timing.
*/
TEST_BEGIN(test_decay_early_wake_branches) {
decay_t decay;
memset(&decay, 0, sizeof(decay));
nstime_t curtime;
nstime_init(&curtime, 0);
uint64_t decay_ms = 1000;
nstime_t decay_nstime;
nstime_init(&decay_nstime, decay_ms * 1000 * 1000);
expect_false(decay_init(&decay, &curtime, (ssize_t)decay_ms),
"Failed to initialize decay");
size_t new_pages = 100;
/*
* Parity trap: with zero new pages the accumulation contributes
* nothing, so decay_npages_purge_in must return 0 regardless of the
* remaining-sleep window. (The early-wake THRESHOLD compare on the
* EXISTING backlog still runs in pac_decay_should_wake_early; that is
* deliberately kept and documented in src/pac.c.)
*/
nstime_t window;
nstime_copy(&window, &decay_nstime);
expect_u64_eq(decay_npages_purge_in(&decay, &window, 0), 0,
"Zero new pages must contribute zero to the purge estimate");
/*
* A remaining-sleep window at/after the full decay period caps the
* estimate at the new-page count (all of them will have decayed).
*/
expect_u64_eq(decay_npages_purge_in(&decay, &window, new_pages),
new_pages, "All new pages should be due within the decay window");
nstime_t beyond;
nstime_copy(&beyond, &decay_nstime);
nstime_add(&beyond, &decay_nstime);
expect_u64_eq(decay_npages_purge_in(&decay, &beyond, new_pages),
new_pages,
"Estimate must cap at the new-page count beyond the decay window");
/*
* No-backlog branch: with a zero threshold, decay_ns_until_purge is
* unbounded (nothing schedulable). This mirrors the indefinite-sleep
* case.
*/
expect_u64_eq(decay_ns_until_purge(&decay, 0, 0),
DECAY_UNBOUNDED_TIME_TO_PURGE,
"Zero threshold must yield an unbounded time-to-purge");
}
TEST_END
int
main(void) {
return test(test_decay_init, test_decay_ms_valid,
test_decay_npages_purge_in, test_decay_maybe_advance_epoch,
test_decay_empty, test_decay, test_decay_ns_until_purge);
test_decay_empty, test_decay, test_decay_ns_until_purge,
test_decay_early_wake_branches);
}

View file

@ -2,6 +2,13 @@
#include "jemalloc/internal/pa.h"
/*
* pac_decay_deferred is JET_EXTERN (test-only; see src/pac.c) and has no public
* header declaration, so declare it locally for the deterministic test below.
*/
extern void pac_decay_deferred(tsdn_t *tsdn, pac_t *pac,
pac_purge_eagerness_t eagerness, pac_deferred_work_result_t *result);
#define PAC_TEST_ARENA_IND 124
static bool pac_test_dalloc_fail;
@ -114,12 +121,14 @@ pac_test_data_init(bool custom_hooks, ssize_t dirty_decay_ms,
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_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");
dirty_decay_ms, muzzy_decay_ms),
"Unexpected pa_shard_init failure");
return data;
}
@ -382,6 +391,45 @@ TEST_BEGIN(test_pac_large_guarded_dalloc_unguards_before_caching) {
}
TEST_END
TEST_BEGIN(test_pac_deferred_never_does_not_purge) {
/*
* PAC_PURGE_NEVER is the eagerness the application free path selects
* when a background thread is enabled, meaning current thread does not
* purge. It is only reachable through the exposed pac_decay_deferred
* primitive (the production entry pac_do_deferred_work picks eagerness
* itself), so this is a deliberate internal test of that critical,
* stable edge.
*/
pac_test_hooks_reset();
/*
* Finite dirty decay so a purge is gated by eagerness, not decay_ms<=0.
*/
pac_test_data_t *data = pac_test_data_init(
/* custom_hooks */ true, /* dirty_decay_ms */ 100 * 1000, -1);
pac_t *pac = &data->shard.pac;
edata_t *edata = pac_alloc_expect(data, PAGE, /* guarded */ false);
size_t ndirty_before_dalloc = ecache_npages_get(&pac->ecache_dirty);
/* Dalloc caches the extent as dirty and requests deferred work. */
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");
size_t ndirty_after_dalloc = ecache_npages_get(&pac->ecache_dirty);
expect_zu_gt(ndirty_after_dalloc, ndirty_before_dalloc,
"Dalloc should add dirty pages");
/* PAC_PURGE_NEVER must leave the dirty pages untouched. */
pac_deferred_work_result_t result;
pac_decay_deferred(tsdn_fetch(), pac, PAC_PURGE_NEVER, &result);
expect_zu_eq(ecache_npages_get(&pac->ecache_dirty), ndirty_after_dalloc,
"PAC_PURGE_NEVER must not purge any dirty pages");
pac_test_data_destroy(data);
}
TEST_END
int
main(void) {
return test_no_reentrancy(test_pac_dirty_muzzy_alloc_priority,
@ -391,5 +439,6 @@ main(void) {
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);
test_pac_large_guarded_dalloc_unguards_before_caching,
test_pac_deferred_never_does_not_purge);
}

View file

@ -1,6 +1,8 @@
#include "test/jemalloc_test.h"
#include "test/arena_util.h"
#include "jemalloc/internal/deferral.h"
#include "jemalloc/internal/pac.h"
#include "jemalloc/internal/ticker.h"
static nstime_monotonic_t *nstime_monotonic_orig;
@ -439,8 +441,35 @@ TEST_BEGIN(test_decay_never) {
}
TEST_END
/*
* pac_time_until_deferred_work is the public PAC scheduling entry the
* background thread uses to decide its next wakeup. Exercised here on a real
* arena through the public interface (no direct decay-state manipulation).
*/
TEST_BEGIN(test_pac_time_until_deferred_work) {
test_skip_if(is_background_thread_enabled());
test_skip_if(opt_hpa);
unsigned arena_ind = do_arena_create(1000, -1);
tsdn_t *tsdn = tsd_tsdn(tsd_fetch());
pac_t *pac = &arena_get(tsdn, arena_ind, false)->pa_shard.pac;
/* Idle: nothing pending -> the scheduler reports the maximum wait. */
expect_u64_eq(pac_time_until_deferred_work(tsdn, pac), DEFERRED_WORK_MAX,
"Idle PAC should defer for the maximum interval");
/* Dirty pages under a finite decay_ms -> a finite wait is scheduled. */
generate_dirty(arena_ind, PAGE);
expect_u64_lt(pac_time_until_deferred_work(tsdn, pac), DEFERRED_WORK_MAX,
"Pending dirty decay should schedule a finite deferred-work time");
do_arena_destroy(arena_ind);
}
TEST_END
int
main(void) {
return test(test_decay_ticks, test_decay_ticker,
test_decay_nonmonotonic, test_decay_now, test_decay_never);
test_decay_nonmonotonic, test_decay_now, test_decay_never,
test_pac_time_until_deferred_work);
}

View file

@ -1,3 +1,3 @@
#!/bin/sh
export MALLOC_CONF="dirty_decay_ms:1000,muzzy_decay_ms:1000,tcache_max:1024"
export MALLOC_CONF="dirty_decay_ms:1000,muzzy_decay_ms:1000,tcache_max:1024,background_thread:false"