@@ -255,6 +255,78 @@ void _mi_page_free_collect(mi_page_t* page, bool force) {
255255 mi_assert_internal (!force || page -> local_free == NULL );
256256}
257257
258+ /* -----------------------------------------------------------
259+ Full-page byte accounting (MI_FULL_PAGE_BYTES)
260+
261+ Maintain `mi_heap_t.full_page_bytes` (bytes of MI_BIN_FULL pages owned by
262+ the heap) and `mi_abandoned_pool_t.full_page_bytes` (bytes of MI_BIN_FULL
263+ pages currently abandoned to that pool). Page weight is
264+ `mi_page_block_size(page) * page->capacity`. Capacity is stable while a
265+ page is in the full queue (`mi_page_extend_free` only runs on non-full
266+ queues), so inc and dec see the same value.
267+
268+ State machine:
269+ to-full : heap += size
270+ from-full : heap -= size
271+ abandon a full : heap -= size; pool += size
272+ reclaim a full : pool -= size; heap += size
273+ free a full : heap -= size
274+
275+ The in_full bit is unconditionally cleared by `mi_page_queue_remove`, so
276+ `_mi_page_abandon` re-sets it after queue_remove to preserve the "this
277+ page's bytes were transferred to the pool" marker through abandonment.
278+ `_mi_page_reclaim` then routes such pages straight to MI_BIN_FULL, so
279+ `mi_page_queue_push` keeps the bit set; subsequent unfull/free fires the
280+ matching dec.
281+
282+ Large/huge pages (block_size > MI_MEDIUM_OBJ_SIZE_MAX) are 1-block pages
283+ in MI_BIN_HUGE; mimalloc never walks that queue on a subsequent alloc, so
284+ it would never call `mi_page_to_full` on them. `_mi_malloc_generic`
285+ therefore eagerly calls `mi_page_to_full` on a freshly-filled huge page
286+ (see the MI_FULL_PAGE_BYTES block at the bottom of that function).
287+ Inc/dec then proceed identically to small/medium pages.
288+
289+ Known minor leak: if a page abandoned-while-full later becomes empty and
290+ then freed, the +size we added on abandon is never subtracted.
291+ ----------------------------------------------------------- */
292+
293+ #if MI_FULL_PAGE_BYTES
294+ static inline intptr_t mi_page_full_size (mi_page_t * page ) {
295+ return (intptr_t )(mi_page_block_size (page ) * (size_t )page -> capacity );
296+ }
297+
298+ static void mi_page_full_inc (mi_page_t * page ) {
299+ mi_atomic_addi (& mi_page_heap (page )-> full_page_bytes , mi_page_full_size (page ));
300+ }
301+
302+ static void mi_page_full_dec (mi_page_t * page ) {
303+ mi_atomic_addi (& mi_page_heap (page )-> full_page_bytes , - mi_page_full_size (page ));
304+ }
305+
306+ // Called from `_mi_page_abandon` *before* the page's heap pointer is cleared.
307+ // Transfers the page's bytes from its heap to the pool that will own the
308+ // abandoned page. No-op if the page is not currently in MI_BIN_FULL.
309+ static void mi_page_full_abandon (mi_page_t * page ) {
310+ if (!mi_page_is_in_full (page )) return ;
311+ intptr_t bytes = mi_page_full_size (page );
312+ mi_heap_t * heap = mi_page_heap (page );
313+ mi_atomic_addi (& heap -> full_page_bytes , - bytes );
314+ mi_atomic_addi (& heap -> tld -> segments .abandoned -> full_page_bytes , bytes );
315+ }
316+
317+ // Called from `_mi_page_reclaim` when a page abandoned-while-full is
318+ // returning to a heap. in_full=true here means "this page's bytes are
319+ // currently in the pool counter from abandon". Transfer them: pool -= size,
320+ // new-heap += size. The caller routes the page directly into MI_BIN_FULL,
321+ // so the in_full bit (and matching dec hook on free/unfull) survives.
322+ static void mi_page_full_reclaim (mi_page_t * page ) {
323+ if (!mi_page_is_in_full (page )) return ;
324+ intptr_t bytes = mi_page_full_size (page );
325+ mi_heap_t * heap = mi_page_heap (page );
326+ mi_atomic_addi (& heap -> tld -> segments .abandoned -> full_page_bytes , - bytes );
327+ mi_atomic_addi (& heap -> full_page_bytes , bytes );
328+ }
329+ #endif // MI_FULL_PAGE_BYTES
258330
259331
260332/* -----------------------------------------------------------
@@ -271,8 +343,24 @@ void _mi_page_reclaim(mi_heap_t* heap, mi_page_t* page) {
271343 mi_assert_internal (_mi_page_segment (page )-> kind != MI_SEGMENT_HUGE );
272344 #endif
273345
274- // TODO: push on full queue immediately if it is full?
275- mi_page_queue_t * pq = mi_page_queue (heap , mi_page_block_size (page ));
346+ mi_page_queue_t * pq ;
347+ #if MI_FULL_PAGE_BYTES
348+ // If the page was abandoned full (in_full preserved as marker), route
349+ // it directly to MI_BIN_FULL. Pushing to the size-bucket queue would
350+ // rely on a later alloc walking that queue to promote it via
351+ // mi_page_to_full -- which happens for small/medium bins but never for
352+ // MI_BIN_HUGE, so a reclaimed full huge page would otherwise leave the
353+ // pool counter without re-crediting any heap. mi_page_full_reclaim
354+ // does the pool-to-heap transfer.
355+ if (mi_page_is_in_full (page )) {
356+ pq = & heap -> pages [MI_BIN_FULL ];
357+ } else {
358+ pq = mi_page_queue (heap , mi_page_block_size (page ));
359+ }
360+ mi_page_full_reclaim (page );
361+ #else
362+ pq = mi_page_queue (heap , mi_page_block_size (page ));
363+ #endif
276364 mi_page_queue_push (heap , pq , page );
277365 _PyMem_mi_page_reclaimed (page );
278366 mi_assert_expensive (_mi_page_is_valid (page ));
@@ -360,8 +448,8 @@ void _mi_page_unfull(mi_page_t* page) {
360448 mi_assert_internal (mi_page_is_in_full (page ));
361449 if (!mi_page_is_in_full (page )) return ;
362450
363- #ifdef Py_GIL_DISABLED
364- _PyMem_mi_page_full_dec (page );
451+ #if MI_FULL_PAGE_BYTES
452+ mi_page_full_dec (page );
365453#endif
366454
367455 mi_heap_t * heap = mi_page_heap (page );
@@ -378,8 +466,8 @@ static void mi_page_to_full(mi_page_t* page, mi_page_queue_t* pq) {
378466 mi_assert_internal (!mi_page_is_in_full (page ));
379467
380468 if (mi_page_is_in_full (page )) return ;
381- #ifdef Py_GIL_DISABLED
382- _PyMem_mi_page_full_inc (page );
469+ #if MI_FULL_PAGE_BYTES
470+ mi_page_full_inc (page );
383471#endif
384472 mi_page_queue_enqueue_from (& mi_page_heap (page )-> pages [MI_BIN_FULL ], pq , page );
385473 _mi_page_free_collect (page ,false); // try to collect right away in case another thread freed just before MI_USE_DELAYED_FREE was set
@@ -398,6 +486,13 @@ void _mi_page_abandon(mi_page_t* page, mi_page_queue_t* pq) {
398486
399487 mi_heap_t * pheap = mi_page_heap (page );
400488
489+ #if MI_FULL_PAGE_BYTES
490+ // Capture in_full while the heap pointer is still valid; transfer the
491+ // bytes from heap counter to pool counter. Must run before
492+ // mi_page_queue_remove, which clears the in_full bit unconditionally.
493+ bool was_in_full = mi_page_is_in_full (page );
494+ mi_page_full_abandon (page );
495+ #endif
401496#ifdef Py_GIL_DISABLED
402497 if (page -> qsbr_node .next != NULL ) {
403498 // remove from QSBR queue, but keep the goal
@@ -413,6 +508,15 @@ void _mi_page_abandon(mi_page_t* page, mi_page_queue_t* pq) {
413508 mi_assert_internal (mi_page_thread_free_flag (page )== MI_NEVER_DELAYED_FREE );
414509 mi_page_set_heap (page , NULL );
415510
511+ #if MI_FULL_PAGE_BYTES
512+ // Preserve the in_full marker through abandonment so `_mi_page_reclaim`'s
513+ // `mi_page_full_reclaim` call can transfer the bytes back to the
514+ // reclaiming heap. Nothing reads in_full on a heap-less page.
515+ if (was_in_full ) {
516+ mi_page_set_in_full (page , true);
517+ }
518+ #endif
519+
416520#if (MI_DEBUG > 1 ) && !MI_TRACK_ENABLED
417521 // check there are no references left..
418522 for (mi_block_t * block = (mi_block_t * )pheap -> thread_delayed_free ; block != NULL ; block = mi_block_nextx (pheap , block , pheap -> keys )) {
@@ -442,12 +546,16 @@ void _mi_page_free(mi_page_t* page, mi_page_queue_t* pq, bool force) {
442546#ifdef Py_GIL_DISABLED
443547 mi_assert_internal (page -> qsbr_goal == 0 );
444548 mi_assert_internal (page -> qsbr_node .next == NULL );
445- // Defensive: a full page whose last block is freed locally goes through
549+ #endif
550+ #if MI_FULL_PAGE_BYTES
551+ // A full page whose last block is freed locally goes through
446552 // _mi_page_retire -> _PyMem_mi_page_maybe_free -> _mi_page_free without
447- // ever calling _mi_page_unfull, so the per-thread full-page counter must
448- // be decremented here to maintain the invariant.
553+ // ever calling _mi_page_unfull, so the heap's full_page_bytes counter
554+ // must be decremented here to maintain the invariant. `heap` is non-NULL
555+ // for any page reaching _mi_page_free (abandoned pages take the
556+ // segment-level cleanup path instead).
449557 if (mi_page_is_in_full (page )) {
450- _PyMem_mi_page_full_dec (page );
558+ mi_page_full_dec (page );
451559 }
452560#endif
453561
@@ -977,14 +1085,28 @@ void* _mi_malloc_generic(mi_heap_t* heap, size_t size, bool zero, size_t huge_al
9771085 mi_assert_internal (mi_page_block_size (page ) >= size );
9781086
9791087 // and try again, this time succeeding! (i.e. this should never recurse through _mi_page_malloc)
1088+ void * p ;
9801089 if mi_unlikely (zero && page -> xblock_size == 0 ) {
9811090 // note: we cannot call _mi_page_malloc with zeroing for huge blocks; we zero it afterwards in that case.
982- void * p = _mi_page_malloc (heap , page , size , false);
1091+ p = _mi_page_malloc (heap , page , size , false);
9831092 mi_assert_internal (p != NULL );
9841093 _mi_memzero_aligned (p , mi_page_usable_block_size (page ));
985- return p ;
9861094 }
9871095 else {
988- return _mi_page_malloc (heap , page , size , zero );
1096+ p = _mi_page_malloc (heap , page , size , zero );
1097+ }
1098+
1099+ #if MI_FULL_PAGE_BYTES
1100+ // Eagerly promote a freshly-filled huge page (1 block per page, in
1101+ // MI_BIN_HUGE) to MI_BIN_FULL so its bytes get counted. See the
1102+ // "Full-page byte accounting" comment block above.
1103+ if (p != NULL && !mi_page_immediate_available (page )) {
1104+ mi_page_queue_t * page_pq = mi_page_queue_of (page );
1105+ if (mi_page_queue_is_huge (page_pq ) && !mi_page_is_in_full (page )) {
1106+ mi_page_to_full (page , page_pq );
1107+ }
9891108 }
1109+ #endif
1110+
1111+ return p ;
9901112}
0 commit comments