From e931730f51ea0fd14c45b31c412beccf01ab3e39 Mon Sep 17 00:00:00 2001 From: guangli-dai Date: Sun, 28 Jun 2026 15:29:38 -0700 Subject: [PATCH] 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. --- Makefile.in | 2 +- test/unit/decay.c | 60 +++++++++++++++++++++- test/unit/pac.c | 57 ++++++++++++++++++-- test/unit/{arena_decay.c => pac_decay.c} | 31 ++++++++++- test/unit/{arena_decay.sh => pac_decay.sh} | 2 +- 5 files changed, 144 insertions(+), 8 deletions(-) rename test/unit/{arena_decay.c => pac_decay.c} (92%) rename test/unit/{arena_decay.sh => pac_decay.sh} (66%) diff --git a/Makefile.in b/Makefile.in index 75cc184a..b151f817 100644 --- a/Makefile.in +++ b/Makefile.in @@ -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 \ diff --git a/test/unit/decay.c b/test/unit/decay.c index 10740a85..05b00579 100644 --- a/test/unit/decay.c +++ b/test/unit/decay.c @@ -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); } diff --git a/test/unit/pac.c b/test/unit/pac.c index e1455979..aad2ded3 100644 --- a/test/unit/pac.c +++ b/test/unit/pac.c @@ -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); } diff --git a/test/unit/arena_decay.c b/test/unit/pac_decay.c similarity index 92% rename from test/unit/arena_decay.c rename to test/unit/pac_decay.c index 99c08ab9..fc9dc839 100644 --- a/test/unit/arena_decay.c +++ b/test/unit/pac_decay.c @@ -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); } diff --git a/test/unit/arena_decay.sh b/test/unit/pac_decay.sh similarity index 66% rename from test/unit/arena_decay.sh rename to test/unit/pac_decay.sh index 52f1b207..f892bb59 100644 --- a/test/unit/arena_decay.sh +++ b/test/unit/pac_decay.sh @@ -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"