1 #include "test/jemalloc_test.h" 2 3 /* 4 * Use multiple shards while keeping the single-threaded shard choice 5 * deterministic. Disable background threads to avoid decay races. 6 */ 7 const char *malloc_conf = 8 "experimental_pac_sec_nshards:2,background_thread:false"; 9 10 static sec_opts_t saved_pac_sec_opts; 11 12 static void 13 pac_sec_test_opts_set(void) { 14 saved_pac_sec_opts = opt_pac_sec_opts; 15 /* 16 * Include cache-oblivious padding so PAC can cache the test size. 17 */ 18 size_t test_extent_size = SC_LARGE_MINCLASS + sz_large_pad; 19 opt_pac_sec_opts.max_alloc = test_extent_size; 20 opt_pac_sec_opts.max_bytes = 4 * test_extent_size; 21 } 22 23 static void 24 pac_sec_test_opts_restore(void) { 25 opt_pac_sec_opts = saved_pac_sec_opts; 26 } 27 28 static void * 29 pinned_extent_alloc(extent_hooks_t *extent_hooks, void *new_addr, 30 size_t size, size_t alignment, bool *zero, bool *commit, 31 unsigned arena_ind) { 32 void *ret = ehooks_default_extent_hooks.alloc( 33 (extent_hooks_t *)&ehooks_default_extent_hooks, new_addr, size, 34 alignment, zero, commit, arena_ind); 35 if (ret == NULL) { 36 return NULL; 37 } 38 if (!*commit) { 39 if (ehooks_default_extent_hooks.commit != NULL 40 && ehooks_default_extent_hooks.commit( 41 (extent_hooks_t *)&ehooks_default_extent_hooks, ret, 42 size, 0, size, arena_ind)) { 43 ehooks_default_extent_hooks.dalloc( 44 (extent_hooks_t *)&ehooks_default_extent_hooks, ret, 45 size, *commit, arena_ind); 46 return NULL; 47 } 48 *commit = true; 49 } 50 return (void *)((uintptr_t)ret | EXTENT_ALLOC_FLAG_PINNED); 51 } 52 53 static void 54 pinned_extent_destroy(extent_hooks_t *extent_hooks, void *addr, size_t size, 55 bool committed, unsigned arena_ind) { 56 ehooks_default_extent_hooks.destroy( 57 (extent_hooks_t *)&ehooks_default_extent_hooks, addr, size, 58 committed, arena_ind); 59 } 60 61 static bool 62 pinned_extent_split(extent_hooks_t *extent_hooks, void *addr, size_t size, 63 size_t size_a, size_t size_b, bool committed, unsigned arena_ind) { 64 return ehooks_default_extent_hooks.split( 65 (extent_hooks_t *)&ehooks_default_extent_hooks, addr, size, size_a, 66 size_b, committed, arena_ind); 67 } 68 69 static bool 70 pinned_extent_merge(extent_hooks_t *extent_hooks, void *addr_a, size_t size_a, 71 void *addr_b, size_t size_b, bool committed, unsigned arena_ind) { 72 return ehooks_default_extent_hooks.merge( 73 (extent_hooks_t *)&ehooks_default_extent_hooks, addr_a, size_a, 74 addr_b, size_b, committed, arena_ind); 75 } 76 77 static extent_hooks_t pinned_hooks = { 78 pinned_extent_alloc, 79 NULL, /* dalloc */ 80 pinned_extent_destroy, 81 NULL, /* commit */ 82 NULL, /* decommit */ 83 NULL, /* purge_lazy */ 84 NULL, /* purge_forced */ 85 pinned_extent_split, 86 pinned_extent_merge 87 }; 88 89 static size_t 90 read_arena_stat(unsigned arena_ind, const char *field) { 91 char cmd[128]; 92 size_t val; 93 size_t sz = sizeof(val); 94 uint64_t epoch = 1; 95 sz = sizeof(epoch); 96 expect_d_eq(mallctl("epoch", NULL, NULL, (void *)&epoch, sz), 0, 97 "Unexpected mallctl failure"); 98 sz = sizeof(val); 99 snprintf(cmd, sizeof(cmd), "stats.arenas.%u.%s", arena_ind, field); 100 expect_d_eq(mallctl(cmd, (void *)&val, &sz, NULL, 0), 0, 101 "Unexpected mallctl failure reading arena stat"); 102 return val; 103 } 104 105 static size_t 106 read_stat(unsigned arena_ind, const char *field) { 107 char cmd[128]; 108 snprintf(cmd, sizeof(cmd), "pac_sec_%s", field); 109 return read_arena_stat(arena_ind, cmd); 110 } 111 112 static size_t 113 read_extents_stat(unsigned arena_ind, pszind_t pszind, const char *field) { 114 char cmd[128]; 115 snprintf(cmd, sizeof(cmd), "extents.%u.%s", pszind, field); 116 return read_arena_stat(arena_ind, cmd); 117 } 118 119 static size_t 120 read_pinned_npages(unsigned arena_ind) { 121 tsd_t *tsd = tsd_fetch(); 122 arena_t *arena = arena_get(tsd_tsdn(tsd), arena_ind, false); 123 expect_ptr_not_null(arena, "arena_get failed"); 124 return ecache_npages_get(&arena->pa_shard.pac.ecache_pinned); 125 } 126 127 static void 128 dirty_decay_ms_set(unsigned arena_ind, ssize_t decay_ms) { 129 char cmd[64]; 130 snprintf(cmd, sizeof(cmd), "arena.%u.dirty_decay_ms", arena_ind); 131 expect_d_eq(mallctl(cmd, NULL, NULL, (void *)&decay_ms, 132 sizeof(decay_ms)), 0, "dirty_decay_ms mallctl failed"); 133 } 134 135 TEST_BEGIN(test_pac_sec_alloc_dalloc_cycle) { 136 test_skip_if(!config_stats); 137 test_skip_if(opt_hpa); 138 139 pac_sec_test_opts_set(); 140 unsigned arena_ind; 141 size_t sz = sizeof(arena_ind); 142 expect_d_eq(mallctl("arenas.create", (void *)&arena_ind, &sz, NULL, 0), 143 0, "Unexpected arenas.create failure"); 144 145 int flags = MALLOCX_ARENA(arena_ind) | MALLOCX_TCACHE_NONE; 146 size_t alloc_size = SC_LARGE_MINCLASS; 147 148 size_t max_bytes; 149 sz = sizeof(max_bytes); 150 expect_d_eq(mallctl("opt.experimental_pac_sec_max_bytes", 151 (void *)&max_bytes, &sz, NULL, 0), 0, 152 "Unexpected mallctl failure"); 153 154 size_t capacity = max_bytes / alloc_size; 155 expect_zu_gt(capacity, 0, "SEC capacity must be > 0 for this test"); 156 157 /* Initial allocation misses SEC. */ 158 void *p1 = mallocx(alloc_size, flags); 159 expect_ptr_not_null(p1, "mallocx failed"); 160 size_t resident_after_alloc = read_arena_stat(arena_ind, "resident"); 161 expect_zu_eq(read_stat(arena_ind, "misses"), 1, 162 "first alloc should miss SEC"); 163 expect_zu_eq(read_stat(arena_ind, "hits"), 0, 164 "no hits yet"); 165 expect_zu_eq(read_stat(arena_ind, "bytes"), 0, 166 "SEC should be empty (extent is active)"); 167 168 /* Freeing the extent caches it in PAC SEC. */ 169 dallocx(p1, flags); 170 size_t cached_after_one = read_stat(arena_ind, "bytes"); 171 expect_zu_gt(cached_after_one, 0, 172 "SEC should cache the freed extent"); 173 expect_zu_ge(read_arena_stat(arena_ind, "pdirty"), 174 cached_after_one >> LG_PAGE, 175 "SEC bytes should count toward dirty page stats"); 176 expect_zu_ge(read_arena_stat(arena_ind, "resident"), 177 resident_after_alloc, 178 "SEC bytes should remain included in resident stats"); 179 /* Use the actual extent size, including size class rounding. */ 180 size_t extent_size = cached_after_one; 181 pszind_t extent_pszind = sz_psz2ind(extent_size); 182 expect_zu_ge(read_extents_stat(arena_ind, extent_pszind, "ndirty"), 1, 183 "SEC extents should count toward extents dirty stats"); 184 expect_zu_ge(read_extents_stat(arena_ind, extent_pszind, "dirty_bytes"), 185 cached_after_one, 186 "SEC bytes should count toward extents dirty byte stats"); 187 expect_zu_eq(read_stat(arena_ind, "dalloc_noflush"), 1, 188 "one dalloc absorbed without flush"); 189 expect_zu_eq(read_stat(arena_ind, "dalloc_flush"), 0, 190 "no flush yet"); 191 192 /* Recompute capacity based on actual extent size. */ 193 capacity = max_bytes / extent_size; 194 expect_zu_gt(capacity, 0, "SEC capacity should be positive"); 195 196 /* Reallocate from SEC. */ 197 void *p2 = mallocx(alloc_size, flags); 198 expect_ptr_not_null(p2, "mallocx failed"); 199 expect_zu_eq(read_stat(arena_ind, "hits"), 1, 200 "second alloc should hit SEC"); 201 expect_zu_eq(read_stat(arena_ind, "misses"), 1, 202 "misses should not increase"); 203 expect_zu_eq(read_stat(arena_ind, "bytes"), 0, 204 "SEC should be empty after hit"); 205 206 dallocx(p2, flags); 207 208 /* Overflow SEC and flush cold extents to ecache_dirty. */ 209 size_t nallocs = capacity + 2; 210 void **ptrs = mallocx(nallocs * sizeof(void *), 211 MALLOCX_TCACHE_NONE); 212 expect_ptr_not_null(ptrs, "metadata alloc failed"); 213 214 for (size_t i = 0; i < nallocs; i++) { 215 ptrs[i] = mallocx(alloc_size, flags); 216 expect_ptr_not_null(ptrs[i], "mallocx %zu failed", i); 217 } 218 for (size_t i = 0; i < nallocs; i++) { 219 dallocx(ptrs[i], flags); 220 } 221 222 size_t noflush = read_stat(arena_ind, "dalloc_noflush"); 223 size_t flush = read_stat(arena_ind, "dalloc_flush"); 224 size_t cached_bytes = read_stat(arena_ind, "bytes"); 225 226 expect_zu_gt(noflush, 1, 227 "most dallocs should be absorbed"); 228 expect_zu_gt(flush, 0, 229 "overflow should trigger at least one flush"); 230 expect_zu_gt(cached_bytes, 0, 231 "SEC should still hold extents after partial flush"); 232 expect_zu_le(cached_bytes, max_bytes, 233 "SEC should not exceed max_bytes"); 234 235 /* A populated SEC serves the next allocation. */ 236 size_t misses_before = read_stat(arena_ind, "misses"); 237 void *p3 = mallocx(alloc_size, flags); 238 expect_ptr_not_null(p3, "mallocx failed"); 239 expect_zu_eq(read_stat(arena_ind, "misses"), misses_before, 240 "alloc from populated SEC should not miss"); 241 dallocx(p3, flags); 242 243 /* Purge flushes SEC entirely. */ 244 char cmd[64]; 245 snprintf(cmd, sizeof(cmd), "arena.%u.purge", arena_ind); 246 expect_d_eq(mallctl(cmd, NULL, NULL, NULL, 0), 0, 247 "purge failed"); 248 expect_zu_eq(read_stat(arena_ind, "bytes"), 0, 249 "SEC should be empty after purge"); 250 251 /* Allocation after purge misses SEC. */ 252 size_t hits_before = read_stat(arena_ind, "hits"); 253 void *p4 = mallocx(alloc_size, flags); 254 expect_ptr_not_null(p4, "mallocx failed"); 255 expect_zu_eq(read_stat(arena_ind, "hits"), hits_before, 256 "alloc after purge should miss SEC"); 257 dallocx(p4, flags); 258 259 dallocx(ptrs, MALLOCX_TCACHE_NONE); 260 snprintf(cmd, sizeof(cmd), "arena.%u.destroy", arena_ind); 261 expect_d_eq(mallctl(cmd, NULL, NULL, NULL, 0), 0, 262 "arena destroy failed"); 263 pac_sec_test_opts_restore(); 264 } 265 TEST_END 266 267 TEST_BEGIN(test_pac_sec_dirty_decay_toggle) { 268 test_skip_if(!config_stats); 269 test_skip_if(opt_hpa); 270 271 pac_sec_test_opts_set(); 272 unsigned arena_ind; 273 size_t sz = sizeof(arena_ind); 274 expect_d_eq(mallctl("arenas.create", (void *)&arena_ind, &sz, NULL, 0), 275 0, "Unexpected arenas.create failure"); 276 277 int flags = MALLOCX_ARENA(arena_ind) | MALLOCX_TCACHE_NONE; 278 size_t alloc_size = SC_LARGE_MINCLASS; 279 280 void *p = mallocx(alloc_size, flags); 281 expect_ptr_not_null(p, "mallocx failed"); 282 dallocx(p, flags); 283 expect_zu_gt(read_stat(arena_ind, "bytes"), 0, 284 "SEC should cache when dirty decay is enabled"); 285 286 dirty_decay_ms_set(arena_ind, 0); 287 expect_zu_eq(read_stat(arena_ind, "bytes"), 0, 288 "disabling dirty decay should flush SEC"); 289 290 p = mallocx(alloc_size, flags); 291 expect_ptr_not_null(p, "mallocx failed"); 292 dallocx(p, flags); 293 expect_zu_eq(read_stat(arena_ind, "bytes"), 0, 294 "SEC should stay disabled while dirty decay is zero"); 295 296 dirty_decay_ms_set(arena_ind, 100); 297 p = mallocx(alloc_size, flags); 298 expect_ptr_not_null(p, "mallocx failed"); 299 dallocx(p, flags); 300 expect_zu_gt(read_stat(arena_ind, "bytes"), 0, 301 "SEC should be usable after dirty decay is re-enabled"); 302 303 char cmd[64]; 304 snprintf(cmd, sizeof(cmd), "arena.%u.destroy", arena_ind); 305 expect_d_eq(mallctl(cmd, NULL, NULL, NULL, 0), 0, 306 "arena destroy failed"); 307 pac_sec_test_opts_restore(); 308 } 309 TEST_END 310 311 TEST_BEGIN(test_pac_sec_flush_pinned) { 312 test_skip_if(!config_stats); 313 test_skip_if(opt_hpa); 314 315 pac_sec_test_opts_set(); 316 unsigned arena_ind; 317 size_t sz = sizeof(arena_ind); 318 extent_hooks_t *hooks_ptr = &pinned_hooks; 319 expect_d_eq(mallctl("arenas.create", (void *)&arena_ind, &sz, 320 &hooks_ptr, sizeof(hooks_ptr)), 0, 321 "Unexpected arenas.create failure"); 322 323 int flags = MALLOCX_ARENA(arena_ind) | MALLOCX_TCACHE_NONE; 324 size_t alloc_size = SC_LARGE_MINCLASS; 325 size_t max_bytes; 326 sz = sizeof(max_bytes); 327 expect_d_eq(mallctl("opt.experimental_pac_sec_max_bytes", 328 (void *)&max_bytes, &sz, NULL, 0), 0, 329 "Unexpected mallctl failure"); 330 331 void *p = mallocx(alloc_size, flags); 332 expect_ptr_not_null(p, "mallocx failed"); 333 size_t resident_after_alloc = read_arena_stat(arena_ind, "resident"); 334 dallocx(p, flags); 335 size_t sec_bytes = read_stat(arena_ind, "bytes"); 336 expect_zu_gt(sec_bytes, 0, "SEC should cache the pinned extent"); 337 expect_zu_ge(read_arena_stat(arena_ind, "pinned"), sec_bytes, 338 "Pinned bytes cached in SEC should count toward pinned stats"); 339 expect_zu_ge(read_arena_stat(arena_ind, "resident"), 340 resident_after_alloc, 341 "Pinned SEC bytes should remain included in resident stats"); 342 343 size_t extent_size = sec_bytes; 344 pszind_t extent_pszind = sz_psz2ind(extent_size); 345 expect_zu_ge(read_extents_stat(arena_ind, extent_pszind, "npinned"), 1, 346 "Pinned SEC extents should count toward extents pinned stats"); 347 expect_zu_ge(read_extents_stat(arena_ind, extent_pszind, "pinned_bytes"), 348 sec_bytes, 349 "Pinned SEC bytes should count toward extents pinned byte stats"); 350 size_t nallocs = max_bytes / extent_size + 2; 351 void **ptrs = mallocx(nallocs * sizeof(void *), MALLOCX_TCACHE_NONE); 352 expect_ptr_not_null(ptrs, "metadata alloc failed"); 353 for (size_t i = 0; i < nallocs; i++) { 354 ptrs[i] = mallocx(alloc_size, flags); 355 expect_ptr_not_null(ptrs[i], "mallocx %zu failed", i); 356 } 357 size_t pinned_before_overflow = read_pinned_npages(arena_ind); 358 for (size_t i = 0; i < nallocs; i++) { 359 dallocx(ptrs[i], flags); 360 } 361 expect_zu_gt(read_pinned_npages(arena_ind), pinned_before_overflow, 362 "SEC overflow should flush pinned extents to ecache_pinned"); 363 364 size_t pinned_before_purge = read_pinned_npages(arena_ind); 365 char cmd[64]; 366 snprintf(cmd, sizeof(cmd), "arena.%u.purge", arena_ind); 367 expect_d_eq(mallctl(cmd, NULL, NULL, NULL, 0), 0, 368 "purge failed"); 369 expect_zu_eq(read_stat(arena_ind, "bytes"), 0, 370 "SEC should be empty after purge"); 371 expect_zu_gt(read_pinned_npages(arena_ind), pinned_before_purge, 372 "PAC SEC purge should flush pinned extents to ecache_pinned"); 373 374 dallocx(ptrs, MALLOCX_TCACHE_NONE); 375 snprintf(cmd, sizeof(cmd), "arena.%u.destroy", arena_ind); 376 expect_d_eq(mallctl(cmd, NULL, NULL, NULL, 0), 0, 377 "arena destroy failed"); 378 pac_sec_test_opts_restore(); 379 } 380 TEST_END 381 382 int 383 main(void) { 384 return test_no_reentrancy( 385 test_pac_sec_alloc_dalloc_cycle, test_pac_sec_dirty_decay_toggle, 386 test_pac_sec_flush_pinned); 387 } 388