1 //===-- primary64.h ---------------------------------------------*- C++ -*-===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 #ifndef SCUDO_PRIMARY64_H_
10 #define SCUDO_PRIMARY64_H_
12 #include "allocator_common.h"
16 #include "local_cache.h"
22 #include "string_utils.h"
23 #include "thread_annotations.h"
25 #include "condition_variable.h"
29 // SizeClassAllocator64 is an allocator tuned for 64-bit address space.
31 // It starts by reserving NumClasses * 2^RegionSizeLog bytes, equally divided in
32 // Regions, specific to each size class. Note that the base of that mapping is
33 // random (based to the platform specific map() capabilities). If
34 // PrimaryEnableRandomOffset is set, each Region actually starts at a random
35 // offset from its base.
37 // Regions are mapped incrementally on demand to fulfill allocation requests,
38 // those mappings being split into equally sized Blocks based on the size class
39 // they belong to. The Blocks created are shuffled to prevent predictable
40 // address patterns (the predictability increases with the size of the Blocks).
42 // The 1st Region (for size class 0) holds the TransferBatches. This is a
43 // structure used to transfer arrays of available pointers from the class size
44 // freelist to the thread specific freelist, and back.
46 // The memory used by this allocator is never unmapped, but can be partially
47 // released if the platform allows for it.
49 template <typename Config
> class SizeClassAllocator64
{
51 typedef typename
Config::Primary::CompactPtrT CompactPtrT
;
52 typedef typename
Config::Primary::SizeClassMap SizeClassMap
;
53 typedef typename ConditionVariableState
<
54 typename
Config::Primary
>::ConditionVariableT ConditionVariableT
;
55 static const uptr CompactPtrScale
= Config::Primary::CompactPtrScale
;
56 static const uptr RegionSizeLog
= Config::Primary::RegionSizeLog
;
57 static const uptr GroupSizeLog
= Config::Primary::GroupSizeLog
;
58 static_assert(RegionSizeLog
>= GroupSizeLog
,
59 "Group size shouldn't be greater than the region size");
60 static const uptr GroupScale
= GroupSizeLog
- CompactPtrScale
;
61 typedef SizeClassAllocator64
<Config
> ThisT
;
62 typedef SizeClassAllocatorLocalCache
<ThisT
> CacheT
;
63 typedef TransferBatch
<ThisT
> TransferBatchT
;
64 typedef BatchGroup
<ThisT
> BatchGroupT
;
66 static_assert(sizeof(BatchGroupT
) <= sizeof(TransferBatchT
),
67 "BatchGroupT uses the same class size as TransferBatchT");
69 static uptr
getSizeByClassId(uptr ClassId
) {
70 return (ClassId
== SizeClassMap::BatchClassId
)
71 ? roundUp(sizeof(TransferBatchT
), 1U << CompactPtrScale
)
72 : SizeClassMap::getSizeByClassId(ClassId
);
75 static bool canAllocate(uptr Size
) { return Size
<= SizeClassMap::MaxSize
; }
77 static bool conditionVariableEnabled() {
78 return ConditionVariableState
<typename
Config::Primary
>::enabled();
81 void init(s32 ReleaseToOsInterval
) NO_THREAD_SAFETY_ANALYSIS
{
82 DCHECK(isAligned(reinterpret_cast<uptr
>(this), alignof(ThisT
)));
84 const uptr PageSize
= getPageSizeCached();
85 const uptr GroupSize
= (1UL << GroupSizeLog
);
86 const uptr PagesInGroup
= GroupSize
/ PageSize
;
87 const uptr MinSizeClass
= getSizeByClassId(1);
88 // When trying to release pages back to memory, visiting smaller size
89 // classes is expensive. Therefore, we only try to release smaller size
90 // classes when the amount of free blocks goes over a certain threshold (See
91 // the comment in releaseToOSMaybe() for more details). For example, for
92 // size class 32, we only do the release when the size of free blocks is
93 // greater than 97% of pages in a group. However, this may introduce another
94 // issue that if the number of free blocks is bouncing between 97% ~ 100%.
95 // Which means we may try many page releases but only release very few of
96 // them (less than 3% in a group). Even though we have
97 // `&ReleaseToOsIntervalMs` which slightly reduce the frequency of these
98 // calls but it will be better to have another guard to mitigate this issue.
100 // Here we add another constraint on the minimum size requirement. The
101 // constraint is determined by the size of in-use blocks in the minimal size
102 // class. Take size class 32 as an example,
104 // +- one memory group -+
105 // +----------------------+------+
106 // | 97% of free blocks | |
107 // +----------------------+------+
111 // * The release size threshold is 97%.
113 // The 3% size in a group is about 7 pages. For two consecutive
114 // releaseToOSMaybe(), we require the difference between `PushedBlocks`
115 // should be greater than 7 pages. This mitigates the page releasing
116 // thrashing which is caused by memory usage bouncing around the threshold.
117 // The smallest size class takes longest time to do the page release so we
118 // use its size of in-use blocks as a heuristic.
119 SmallerBlockReleasePageDelta
=
120 PagesInGroup
* (1 + MinSizeClass
/ 16U) / 100;
122 // Reserve the space required for the Primary.
123 CHECK(ReservedMemory
.create(/*Addr=*/0U, PrimarySize
,
124 "scudo:primary_reserve"));
125 PrimaryBase
= ReservedMemory
.getBase();
126 DCHECK_NE(PrimaryBase
, 0U);
129 const u64 Time
= getMonotonicTimeFast();
130 if (!getRandom(reinterpret_cast<void *>(&Seed
), sizeof(Seed
)))
131 Seed
= static_cast<u32
>(Time
^ (PrimaryBase
>> 12));
133 for (uptr I
= 0; I
< NumClasses
; I
++) {
134 RegionInfo
*Region
= getRegionInfo(I
);
136 // The actual start of a region is offset by a random number of pages
137 // when PrimaryEnableRandomOffset is set.
138 Region
->RegionBeg
= (PrimaryBase
+ (I
<< RegionSizeLog
)) +
139 (Config::Primary::EnableRandomOffset
140 ? ((getRandomModN(&Seed
, 16) + 1) * PageSize
)
142 Region
->RandState
= getRandomU32(&Seed
);
143 // Releasing small blocks is expensive, set a higher threshold to avoid
144 // frequent page releases.
145 if (isSmallBlock(getSizeByClassId(I
)))
146 Region
->TryReleaseThreshold
= PageSize
* SmallerBlockReleasePageDelta
;
148 Region
->TryReleaseThreshold
= PageSize
;
149 Region
->ReleaseInfo
.LastReleaseAtNs
= Time
;
151 Region
->MemMapInfo
.MemMap
= ReservedMemory
.dispatch(
152 PrimaryBase
+ (I
<< RegionSizeLog
), RegionSize
);
153 CHECK(Region
->MemMapInfo
.MemMap
.isAllocated());
155 shuffle(RegionInfoArray
, NumClasses
, &Seed
);
157 // The binding should be done after region shuffling so that it won't bind
158 // the FLLock from the wrong region.
159 for (uptr I
= 0; I
< NumClasses
; I
++)
160 getRegionInfo(I
)->FLLockCV
.bindTestOnly(getRegionInfo(I
)->FLLock
);
162 setOption(Option::ReleaseInterval
, static_cast<sptr
>(ReleaseToOsInterval
));
165 void unmapTestOnly() NO_THREAD_SAFETY_ANALYSIS
{
166 for (uptr I
= 0; I
< NumClasses
; I
++) {
167 RegionInfo
*Region
= getRegionInfo(I
);
171 ReservedMemory
.release();
175 // When all blocks are freed, it has to be the same size as `AllocatedUser`.
176 void verifyAllBlocksAreReleasedTestOnly() {
177 // `BatchGroup` and `TransferBatch` also use the blocks from BatchClass.
178 uptr BatchClassUsedInFreeLists
= 0;
179 for (uptr I
= 0; I
< NumClasses
; I
++) {
180 // We have to count BatchClassUsedInFreeLists in other regions first.
181 if (I
== SizeClassMap::BatchClassId
)
183 RegionInfo
*Region
= getRegionInfo(I
);
184 ScopedLock
ML(Region
->MMLock
);
185 ScopedLock
FL(Region
->FLLock
);
186 const uptr BlockSize
= getSizeByClassId(I
);
187 uptr TotalBlocks
= 0;
188 for (BatchGroupT
&BG
: Region
->FreeListInfo
.BlockList
) {
189 // `BG::Batches` are `TransferBatches`. +1 for `BatchGroup`.
190 BatchClassUsedInFreeLists
+= BG
.Batches
.size() + 1;
191 for (const auto &It
: BG
.Batches
)
192 TotalBlocks
+= It
.getCount();
195 DCHECK_EQ(TotalBlocks
, Region
->MemMapInfo
.AllocatedUser
/ BlockSize
);
196 DCHECK_EQ(Region
->FreeListInfo
.PushedBlocks
,
197 Region
->FreeListInfo
.PoppedBlocks
);
200 RegionInfo
*Region
= getRegionInfo(SizeClassMap::BatchClassId
);
201 ScopedLock
ML(Region
->MMLock
);
202 ScopedLock
FL(Region
->FLLock
);
203 const uptr BlockSize
= getSizeByClassId(SizeClassMap::BatchClassId
);
204 uptr TotalBlocks
= 0;
205 for (BatchGroupT
&BG
: Region
->FreeListInfo
.BlockList
) {
206 if (LIKELY(!BG
.Batches
.empty())) {
207 for (const auto &It
: BG
.Batches
)
208 TotalBlocks
+= It
.getCount();
210 // `BatchGroup` with empty freelist doesn't have `TransferBatch` record
215 DCHECK_EQ(TotalBlocks
+ BatchClassUsedInFreeLists
,
216 Region
->MemMapInfo
.AllocatedUser
/ BlockSize
);
217 DCHECK_GE(Region
->FreeListInfo
.PoppedBlocks
,
218 Region
->FreeListInfo
.PushedBlocks
);
219 const uptr BlocksInUse
=
220 Region
->FreeListInfo
.PoppedBlocks
- Region
->FreeListInfo
.PushedBlocks
;
221 DCHECK_EQ(BlocksInUse
, BatchClassUsedInFreeLists
);
224 // Note that the `MaxBlockCount` will be used when we support arbitrary blocks
225 // count. Now it's the same as the number of blocks stored in the
227 u16
popBlocks(CacheT
*C
, uptr ClassId
, CompactPtrT
*ToArray
,
228 UNUSED
const u16 MaxBlockCount
) {
229 TransferBatchT
*B
= popBatch(C
, ClassId
);
233 const u16 Count
= B
->getCount();
234 DCHECK_GT(Count
, 0U);
235 B
->moveToArray(ToArray
);
237 if (ClassId
!= SizeClassMap::BatchClassId
)
238 C
->deallocate(SizeClassMap::BatchClassId
, B
);
243 TransferBatchT
*popBatch(CacheT
*C
, uptr ClassId
) {
244 DCHECK_LT(ClassId
, NumClasses
);
245 RegionInfo
*Region
= getRegionInfo(ClassId
);
248 ScopedLock
L(Region
->FLLock
);
249 TransferBatchT
*B
= popBatchImpl(C
, ClassId
, Region
);
254 bool ReportRegionExhausted
= false;
255 TransferBatchT
*B
= nullptr;
257 if (conditionVariableEnabled()) {
258 B
= popBatchWithCV(C
, ClassId
, Region
, ReportRegionExhausted
);
261 // When two threads compete for `Region->MMLock`, we only want one of
262 // them to call populateFreeListAndPopBatch(). To avoid both of them
263 // doing that, always check the freelist before mapping new pages.
264 ScopedLock
ML(Region
->MMLock
);
266 ScopedLock
FL(Region
->FLLock
);
267 if ((B
= popBatchImpl(C
, ClassId
, Region
)))
271 const bool RegionIsExhausted
= Region
->Exhausted
;
272 if (!RegionIsExhausted
)
273 B
= populateFreeListAndPopBatch(C
, ClassId
, Region
);
274 ReportRegionExhausted
= !RegionIsExhausted
&& Region
->Exhausted
;
279 if (UNLIKELY(ReportRegionExhausted
)) {
280 Printf("Can't populate more pages for size class %zu.\n",
281 getSizeByClassId(ClassId
));
283 // Theoretically, BatchClass shouldn't be used up. Abort immediately when
285 if (ClassId
== SizeClassMap::BatchClassId
)
286 reportOutOfBatchClass();
292 // Push the array of free blocks to the designated batch group.
293 void pushBlocks(CacheT
*C
, uptr ClassId
, CompactPtrT
*Array
, u32 Size
) {
294 DCHECK_LT(ClassId
, NumClasses
);
297 RegionInfo
*Region
= getRegionInfo(ClassId
);
298 if (ClassId
== SizeClassMap::BatchClassId
) {
299 ScopedLock
L(Region
->FLLock
);
300 pushBatchClassBlocks(Region
, Array
, Size
);
301 if (conditionVariableEnabled())
302 Region
->FLLockCV
.notifyAll(Region
->FLLock
);
306 // TODO(chiahungduan): Consider not doing grouping if the group size is not
307 // greater than the block size with a certain scale.
309 bool SameGroup
= true;
310 if (GroupSizeLog
< RegionSizeLog
) {
311 // Sort the blocks so that blocks belonging to the same group can be
313 for (u32 I
= 1; I
< Size
; ++I
) {
314 if (compactPtrGroup(Array
[I
- 1]) != compactPtrGroup(Array
[I
]))
316 CompactPtrT Cur
= Array
[I
];
318 while (J
> 0 && compactPtrGroup(Cur
) < compactPtrGroup(Array
[J
- 1])) {
319 Array
[J
] = Array
[J
- 1];
327 ScopedLock
L(Region
->FLLock
);
328 pushBlocksImpl(C
, ClassId
, Region
, Array
, Size
, SameGroup
);
329 if (conditionVariableEnabled())
330 Region
->FLLockCV
.notifyAll(Region
->FLLock
);
334 void disable() NO_THREAD_SAFETY_ANALYSIS
{
335 // The BatchClassId must be locked last since other classes can use it.
336 for (sptr I
= static_cast<sptr
>(NumClasses
) - 1; I
>= 0; I
--) {
337 if (static_cast<uptr
>(I
) == SizeClassMap::BatchClassId
)
339 getRegionInfo(static_cast<uptr
>(I
))->MMLock
.lock();
340 getRegionInfo(static_cast<uptr
>(I
))->FLLock
.lock();
342 getRegionInfo(SizeClassMap::BatchClassId
)->MMLock
.lock();
343 getRegionInfo(SizeClassMap::BatchClassId
)->FLLock
.lock();
346 void enable() NO_THREAD_SAFETY_ANALYSIS
{
347 getRegionInfo(SizeClassMap::BatchClassId
)->FLLock
.unlock();
348 getRegionInfo(SizeClassMap::BatchClassId
)->MMLock
.unlock();
349 for (uptr I
= 0; I
< NumClasses
; I
++) {
350 if (I
== SizeClassMap::BatchClassId
)
352 getRegionInfo(I
)->FLLock
.unlock();
353 getRegionInfo(I
)->MMLock
.unlock();
357 template <typename F
> void iterateOverBlocks(F Callback
) {
358 for (uptr I
= 0; I
< NumClasses
; I
++) {
359 if (I
== SizeClassMap::BatchClassId
)
361 RegionInfo
*Region
= getRegionInfo(I
);
362 // TODO: The call of `iterateOverBlocks` requires disabling
363 // SizeClassAllocator64. We may consider locking each region on demand
365 Region
->FLLock
.assertHeld();
366 Region
->MMLock
.assertHeld();
367 const uptr BlockSize
= getSizeByClassId(I
);
368 const uptr From
= Region
->RegionBeg
;
369 const uptr To
= From
+ Region
->MemMapInfo
.AllocatedUser
;
370 for (uptr Block
= From
; Block
< To
; Block
+= BlockSize
)
375 void getStats(ScopedString
*Str
) {
376 // TODO(kostyak): get the RSS per region.
377 uptr TotalMapped
= 0;
378 uptr PoppedBlocks
= 0;
379 uptr PushedBlocks
= 0;
380 for (uptr I
= 0; I
< NumClasses
; I
++) {
381 RegionInfo
*Region
= getRegionInfo(I
);
383 ScopedLock
L(Region
->MMLock
);
384 TotalMapped
+= Region
->MemMapInfo
.MappedUser
;
387 ScopedLock
L(Region
->FLLock
);
388 PoppedBlocks
+= Region
->FreeListInfo
.PoppedBlocks
;
389 PushedBlocks
+= Region
->FreeListInfo
.PushedBlocks
;
392 Str
->append("Stats: SizeClassAllocator64: %zuM mapped (%uM rss) in %zu "
393 "allocations; remains %zu\n",
394 TotalMapped
>> 20, 0U, PoppedBlocks
,
395 PoppedBlocks
- PushedBlocks
);
397 for (uptr I
= 0; I
< NumClasses
; I
++) {
398 RegionInfo
*Region
= getRegionInfo(I
);
399 ScopedLock
L1(Region
->MMLock
);
400 ScopedLock
L2(Region
->FLLock
);
401 getStats(Str
, I
, Region
);
405 void getFragmentationInfo(ScopedString
*Str
) {
407 "Fragmentation Stats: SizeClassAllocator64: page size = %zu bytes\n",
408 getPageSizeCached());
410 for (uptr I
= 1; I
< NumClasses
; I
++) {
411 RegionInfo
*Region
= getRegionInfo(I
);
412 ScopedLock
L(Region
->MMLock
);
413 getRegionFragmentationInfo(Region
, I
, Str
);
417 bool setOption(Option O
, sptr Value
) {
418 if (O
== Option::ReleaseInterval
) {
419 const s32 Interval
= Max(Min(static_cast<s32
>(Value
),
420 Config::Primary::MaxReleaseToOsIntervalMs
),
421 Config::Primary::MinReleaseToOsIntervalMs
);
422 atomic_store_relaxed(&ReleaseToOsIntervalMs
, Interval
);
425 // Not supported by the Primary, but not an error either.
429 uptr
tryReleaseToOS(uptr ClassId
, ReleaseToOS ReleaseType
) {
430 RegionInfo
*Region
= getRegionInfo(ClassId
);
431 // Note that the tryLock() may fail spuriously, given that it should rarely
432 // happen and page releasing is fine to skip, we don't take certain
433 // approaches to ensure one page release is done.
434 if (Region
->MMLock
.tryLock()) {
435 uptr BytesReleased
= releaseToOSMaybe(Region
, ClassId
, ReleaseType
);
436 Region
->MMLock
.unlock();
437 return BytesReleased
;
442 uptr
releaseToOS(ReleaseToOS ReleaseType
) {
443 uptr TotalReleasedBytes
= 0;
444 for (uptr I
= 0; I
< NumClasses
; I
++) {
445 if (I
== SizeClassMap::BatchClassId
)
447 RegionInfo
*Region
= getRegionInfo(I
);
448 ScopedLock
L(Region
->MMLock
);
449 TotalReleasedBytes
+= releaseToOSMaybe(Region
, I
, ReleaseType
);
451 return TotalReleasedBytes
;
454 const char *getRegionInfoArrayAddress() const {
455 return reinterpret_cast<const char *>(RegionInfoArray
);
458 static uptr
getRegionInfoArraySize() { return sizeof(RegionInfoArray
); }
460 uptr
getCompactPtrBaseByClassId(uptr ClassId
) {
461 return getRegionInfo(ClassId
)->RegionBeg
;
464 CompactPtrT
compactPtr(uptr ClassId
, uptr Ptr
) {
465 DCHECK_LE(ClassId
, SizeClassMap::LargestClassId
);
466 return compactPtrInternal(getCompactPtrBaseByClassId(ClassId
), Ptr
);
469 void *decompactPtr(uptr ClassId
, CompactPtrT CompactPtr
) {
470 DCHECK_LE(ClassId
, SizeClassMap::LargestClassId
);
471 return reinterpret_cast<void *>(
472 decompactPtrInternal(getCompactPtrBaseByClassId(ClassId
), CompactPtr
));
475 static BlockInfo
findNearestBlock(const char *RegionInfoData
,
476 uptr Ptr
) NO_THREAD_SAFETY_ANALYSIS
{
477 const RegionInfo
*RegionInfoArray
=
478 reinterpret_cast<const RegionInfo
*>(RegionInfoData
);
481 uptr MinDistance
= -1UL;
482 for (uptr I
= 0; I
!= NumClasses
; ++I
) {
483 if (I
== SizeClassMap::BatchClassId
)
485 uptr Begin
= RegionInfoArray
[I
].RegionBeg
;
486 // TODO(chiahungduan): In fact, We need to lock the RegionInfo::MMLock.
487 // However, the RegionInfoData is passed with const qualifier and lock the
488 // mutex requires modifying RegionInfoData, which means we need to remove
489 // the const qualifier. This may lead to another undefined behavior (The
490 // first one is accessing `AllocatedUser` without locking. It's better to
491 // pass `RegionInfoData` as `void *` then we can lock the mutex properly.
492 uptr End
= Begin
+ RegionInfoArray
[I
].MemMapInfo
.AllocatedUser
;
493 if (Begin
> End
|| End
- Begin
< SizeClassMap::getSizeByClassId(I
))
500 RegionDistance
= Ptr
- End
;
502 RegionDistance
= Begin
- Ptr
;
505 if (RegionDistance
< MinDistance
) {
506 MinDistance
= RegionDistance
;
512 if (MinDistance
<= 8192) {
513 B
.RegionBegin
= RegionInfoArray
[ClassId
].RegionBeg
;
515 B
.RegionBegin
+ RegionInfoArray
[ClassId
].MemMapInfo
.AllocatedUser
;
516 B
.BlockSize
= SizeClassMap::getSizeByClassId(ClassId
);
518 B
.RegionBegin
+ uptr(sptr(Ptr
- B
.RegionBegin
) / sptr(B
.BlockSize
) *
520 while (B
.BlockBegin
< B
.RegionBegin
)
521 B
.BlockBegin
+= B
.BlockSize
;
522 while (B
.RegionEnd
< B
.BlockBegin
+ B
.BlockSize
)
523 B
.BlockBegin
-= B
.BlockSize
;
528 AtomicOptions Options
;
531 static const uptr RegionSize
= 1UL << RegionSizeLog
;
532 static const uptr NumClasses
= SizeClassMap::NumClasses
;
533 static const uptr PrimarySize
= RegionSize
* NumClasses
;
535 static const uptr MapSizeIncrement
= Config::Primary::MapSizeIncrement
;
536 // Fill at most this number of batches from the newly map'd memory.
537 static const u32 MaxNumBatches
= SCUDO_ANDROID
? 4U : 8U;
539 struct ReleaseToOsInfo
{
540 uptr BytesInFreeListAtLastCheckpoint
;
542 uptr LastReleasedBytes
;
547 SinglyLinkedList
<BatchGroupT
> BlockList
= {};
548 uptr PoppedBlocks
= 0;
549 uptr PushedBlocks
= 0;
554 // Bytes mapped for user memory.
556 // Bytes allocated for user memory.
557 uptr AllocatedUser
= 0;
560 struct UnpaddedRegionInfo
{
561 // Mutex for operations on freelist
563 ConditionVariableT FLLockCV
GUARDED_BY(FLLock
);
564 // Mutex for memmap operations
565 HybridMutex MMLock
ACQUIRED_BEFORE(FLLock
);
566 // `RegionBeg` is initialized before thread creation and won't be changed.
568 u32 RandState
GUARDED_BY(MMLock
) = 0;
569 BlocksInfo FreeListInfo
GUARDED_BY(FLLock
);
570 PagesInfo MemMapInfo
GUARDED_BY(MMLock
);
571 // The minimum size of pushed blocks to trigger page release.
572 uptr TryReleaseThreshold
GUARDED_BY(MMLock
) = 0;
573 ReleaseToOsInfo ReleaseInfo
GUARDED_BY(MMLock
) = {};
574 bool Exhausted
GUARDED_BY(MMLock
) = false;
575 bool isPopulatingFreeList
GUARDED_BY(FLLock
) = false;
577 struct RegionInfo
: UnpaddedRegionInfo
{
578 char Padding
[SCUDO_CACHE_LINE_SIZE
-
579 (sizeof(UnpaddedRegionInfo
) % SCUDO_CACHE_LINE_SIZE
)] = {};
581 static_assert(sizeof(RegionInfo
) % SCUDO_CACHE_LINE_SIZE
== 0, "");
583 RegionInfo
*getRegionInfo(uptr ClassId
) {
584 DCHECK_LT(ClassId
, NumClasses
);
585 return &RegionInfoArray
[ClassId
];
588 uptr
getRegionBaseByClassId(uptr ClassId
) {
589 return roundDown(getRegionInfo(ClassId
)->RegionBeg
- PrimaryBase
,
594 static CompactPtrT
compactPtrInternal(uptr Base
, uptr Ptr
) {
595 return static_cast<CompactPtrT
>((Ptr
- Base
) >> CompactPtrScale
);
598 static uptr
decompactPtrInternal(uptr Base
, CompactPtrT CompactPtr
) {
599 return Base
+ (static_cast<uptr
>(CompactPtr
) << CompactPtrScale
);
602 static uptr
compactPtrGroup(CompactPtrT CompactPtr
) {
603 const uptr Mask
= (static_cast<uptr
>(1) << GroupScale
) - 1;
604 return static_cast<uptr
>(CompactPtr
) & ~Mask
;
606 static uptr
decompactGroupBase(uptr Base
, uptr CompactPtrGroupBase
) {
607 DCHECK_EQ(CompactPtrGroupBase
% (static_cast<uptr
>(1) << (GroupScale
)), 0U);
608 return Base
+ (CompactPtrGroupBase
<< CompactPtrScale
);
611 ALWAYS_INLINE
static bool isSmallBlock(uptr BlockSize
) {
612 const uptr PageSize
= getPageSizeCached();
613 return BlockSize
< PageSize
/ 16U;
616 ALWAYS_INLINE
static bool isLargeBlock(uptr BlockSize
) {
617 const uptr PageSize
= getPageSizeCached();
618 return BlockSize
> PageSize
;
621 void pushBatchClassBlocks(RegionInfo
*Region
, CompactPtrT
*Array
, u32 Size
)
622 REQUIRES(Region
->FLLock
) {
623 DCHECK_EQ(Region
, getRegionInfo(SizeClassMap::BatchClassId
));
625 // Free blocks are recorded by TransferBatch in freelist for all
626 // size-classes. In addition, TransferBatch is allocated from BatchClassId.
627 // In order not to use additional block to record the free blocks in
628 // BatchClassId, they are self-contained. I.e., A TransferBatch records the
629 // block address of itself. See the figure below:
631 // TransferBatch at 0xABCD
632 // +----------------------------+
633 // | Free blocks' addr |
634 // | +------+------+------+ |
635 // | |0xABCD|... |... | |
636 // | +------+------+------+ |
637 // +----------------------------+
639 // When we allocate all the free blocks in the TransferBatch, the block used
640 // by TransferBatch is also free for use. We don't need to recycle the
641 // TransferBatch. Note that the correctness is maintained by the invariant,
643 // The unit of each popBatch() request is entire TransferBatch. Return
644 // part of the blocks in a TransferBatch is invalid.
646 // This ensures that TransferBatch won't leak the address itself while it's
647 // still holding other valid data.
649 // Besides, BatchGroup is also allocated from BatchClassId and has its
650 // address recorded in the TransferBatch too. To maintain the correctness,
652 // The address of BatchGroup is always recorded in the last TransferBatch
653 // in the freelist (also imply that the freelist should only be
654 // updated with push_front). Once the last TransferBatch is popped,
655 // the block used by BatchGroup is also free for use.
657 // With this approach, the blocks used by BatchGroup and TransferBatch are
658 // reusable and don't need additional space for them.
660 Region
->FreeListInfo
.PushedBlocks
+= Size
;
661 BatchGroupT
*BG
= Region
->FreeListInfo
.BlockList
.front();
664 // Construct `BatchGroup` on the last element.
665 BG
= reinterpret_cast<BatchGroupT
*>(
666 decompactPtr(SizeClassMap::BatchClassId
, Array
[Size
- 1]));
669 // BatchClass hasn't enabled memory group. Use `0` to indicate there's no
670 // memory group here.
671 BG
->CompactPtrGroupBase
= 0;
672 // `BG` is also the block of BatchClassId. Note that this is different
673 // from `CreateGroup` in `pushBlocksImpl`
674 BG
->PushedBlocks
= 1;
675 BG
->BytesInBGAtLastCheckpoint
= 0;
676 BG
->MaxCachedPerBatch
=
677 CacheT::getMaxCached(getSizeByClassId(SizeClassMap::BatchClassId
));
679 Region
->FreeListInfo
.BlockList
.push_front(BG
);
682 if (UNLIKELY(Size
== 0))
685 // This happens under 2 cases.
686 // 1. just allocated a new `BatchGroup`.
687 // 2. Only 1 block is pushed when the freelist is empty.
688 if (BG
->Batches
.empty()) {
689 // Construct the `TransferBatch` on the last element.
690 TransferBatchT
*TB
= reinterpret_cast<TransferBatchT
*>(
691 decompactPtr(SizeClassMap::BatchClassId
, Array
[Size
- 1]));
693 // As mentioned above, addresses of `TransferBatch` and `BatchGroup` are
694 // recorded in the TransferBatch.
695 TB
->add(Array
[Size
- 1]);
697 compactPtr(SizeClassMap::BatchClassId
, reinterpret_cast<uptr
>(BG
)));
699 DCHECK_EQ(BG
->PushedBlocks
, 1U);
700 // `TB` is also the block of BatchClassId.
701 BG
->PushedBlocks
+= 1;
702 BG
->Batches
.push_front(TB
);
705 TransferBatchT
*CurBatch
= BG
->Batches
.front();
706 DCHECK_NE(CurBatch
, nullptr);
708 for (u32 I
= 0; I
< Size
;) {
710 static_cast<u16
>(BG
->MaxCachedPerBatch
- CurBatch
->getCount());
711 if (UnusedSlots
== 0) {
712 CurBatch
= reinterpret_cast<TransferBatchT
*>(
713 decompactPtr(SizeClassMap::BatchClassId
, Array
[I
]));
716 CurBatch
->add(Array
[I
]);
718 // TODO(chiahungduan): Avoid the use of push_back() in `Batches` of
720 BG
->Batches
.push_front(CurBatch
);
721 UnusedSlots
= static_cast<u16
>(BG
->MaxCachedPerBatch
- 1);
723 // `UnusedSlots` is u16 so the result will be also fit in u16.
724 const u16 AppendSize
= static_cast<u16
>(Min
<u32
>(UnusedSlots
, Size
- I
));
725 CurBatch
->appendFromArray(&Array
[I
], AppendSize
);
729 BG
->PushedBlocks
+= Size
;
732 // Push the blocks to their batch group. The layout will be like,
734 // FreeListInfo.BlockList - > BG -> BG -> BG
742 // Each BlockGroup(BG) will associate with unique group id and the free blocks
743 // are managed by a list of TransferBatch(TB). To reduce the time of inserting
744 // blocks, BGs are sorted and the input `Array` are supposed to be sorted so
745 // that we can get better performance of maintaining sorted property.
746 // Use `SameGroup=true` to indicate that all blocks in the array are from the
747 // same group then we will skip checking the group id of each block.
748 void pushBlocksImpl(CacheT
*C
, uptr ClassId
, RegionInfo
*Region
,
749 CompactPtrT
*Array
, u32 Size
, bool SameGroup
= false)
750 REQUIRES(Region
->FLLock
) {
751 DCHECK_NE(ClassId
, SizeClassMap::BatchClassId
);
754 auto CreateGroup
= [&](uptr CompactPtrGroupBase
) {
756 reinterpret_cast<BatchGroupT
*>(C
->getBatchClassBlock());
759 reinterpret_cast<TransferBatchT
*>(C
->getBatchClassBlock());
762 BG
->CompactPtrGroupBase
= CompactPtrGroupBase
;
763 BG
->Batches
.push_front(TB
);
764 BG
->PushedBlocks
= 0;
765 BG
->BytesInBGAtLastCheckpoint
= 0;
766 BG
->MaxCachedPerBatch
= CacheT::getMaxCached(getSizeByClassId(ClassId
));
771 auto InsertBlocks
= [&](BatchGroupT
*BG
, CompactPtrT
*Array
, u32 Size
) {
772 SinglyLinkedList
<TransferBatchT
> &Batches
= BG
->Batches
;
773 TransferBatchT
*CurBatch
= Batches
.front();
774 DCHECK_NE(CurBatch
, nullptr);
776 for (u32 I
= 0; I
< Size
;) {
777 DCHECK_GE(BG
->MaxCachedPerBatch
, CurBatch
->getCount());
779 static_cast<u16
>(BG
->MaxCachedPerBatch
- CurBatch
->getCount());
780 if (UnusedSlots
== 0) {
782 reinterpret_cast<TransferBatchT
*>(C
->getBatchClassBlock());
784 Batches
.push_front(CurBatch
);
785 UnusedSlots
= BG
->MaxCachedPerBatch
;
787 // `UnusedSlots` is u16 so the result will be also fit in u16.
788 u16 AppendSize
= static_cast<u16
>(Min
<u32
>(UnusedSlots
, Size
- I
));
789 CurBatch
->appendFromArray(&Array
[I
], AppendSize
);
793 BG
->PushedBlocks
+= Size
;
796 Region
->FreeListInfo
.PushedBlocks
+= Size
;
797 BatchGroupT
*Cur
= Region
->FreeListInfo
.BlockList
.front();
799 // In the following, `Cur` always points to the BatchGroup for blocks that
800 // will be pushed next. `Prev` is the element right before `Cur`.
801 BatchGroupT
*Prev
= nullptr;
803 while (Cur
!= nullptr &&
804 compactPtrGroup(Array
[0]) > Cur
->CompactPtrGroupBase
) {
809 if (Cur
== nullptr ||
810 compactPtrGroup(Array
[0]) != Cur
->CompactPtrGroupBase
) {
811 Cur
= CreateGroup(compactPtrGroup(Array
[0]));
813 Region
->FreeListInfo
.BlockList
.push_front(Cur
);
815 Region
->FreeListInfo
.BlockList
.insert(Prev
, Cur
);
818 // All the blocks are from the same group, just push without checking group
821 for (u32 I
= 0; I
< Size
; ++I
)
822 DCHECK_EQ(compactPtrGroup(Array
[I
]), Cur
->CompactPtrGroupBase
);
824 InsertBlocks(Cur
, Array
, Size
);
828 // The blocks are sorted by group id. Determine the segment of group and
829 // push them to their group together.
831 for (u32 I
= 1; I
< Size
; ++I
) {
832 if (compactPtrGroup(Array
[I
- 1]) != compactPtrGroup(Array
[I
])) {
833 DCHECK_EQ(compactPtrGroup(Array
[I
- 1]), Cur
->CompactPtrGroupBase
);
834 InsertBlocks(Cur
, Array
+ I
- Count
, Count
);
836 while (Cur
!= nullptr &&
837 compactPtrGroup(Array
[I
]) > Cur
->CompactPtrGroupBase
) {
842 if (Cur
== nullptr ||
843 compactPtrGroup(Array
[I
]) != Cur
->CompactPtrGroupBase
) {
844 Cur
= CreateGroup(compactPtrGroup(Array
[I
]));
845 DCHECK_NE(Prev
, nullptr);
846 Region
->FreeListInfo
.BlockList
.insert(Prev
, Cur
);
855 InsertBlocks(Cur
, Array
+ Size
- Count
, Count
);
858 TransferBatchT
*popBatchWithCV(CacheT
*C
, uptr ClassId
, RegionInfo
*Region
,
859 bool &ReportRegionExhausted
) {
860 TransferBatchT
*B
= nullptr;
863 // We only expect one thread doing the freelist refillment and other
864 // threads will be waiting for either the completion of the
865 // `populateFreeListAndPopBatch()` or `pushBlocks()` called by other
867 bool PopulateFreeList
= false;
869 ScopedLock
FL(Region
->FLLock
);
870 if (!Region
->isPopulatingFreeList
) {
871 Region
->isPopulatingFreeList
= true;
872 PopulateFreeList
= true;
876 if (PopulateFreeList
) {
877 ScopedLock
ML(Region
->MMLock
);
879 const bool RegionIsExhausted
= Region
->Exhausted
;
880 if (!RegionIsExhausted
)
881 B
= populateFreeListAndPopBatch(C
, ClassId
, Region
);
882 ReportRegionExhausted
= !RegionIsExhausted
&& Region
->Exhausted
;
885 // Before reacquiring the `FLLock`, the freelist may be used up again
886 // and some threads are waiting for the freelist refillment by the
887 // current thread. It's important to set
888 // `Region->isPopulatingFreeList` to false so the threads about to
889 // sleep will notice the status change.
890 ScopedLock
FL(Region
->FLLock
);
891 Region
->isPopulatingFreeList
= false;
892 Region
->FLLockCV
.notifyAll(Region
->FLLock
);
898 // At here, there are two preconditions to be met before waiting,
899 // 1. The freelist is empty.
900 // 2. Region->isPopulatingFreeList == true, i.e, someone is still doing
901 // `populateFreeListAndPopBatch()`.
903 // Note that it has the chance that freelist is empty but
904 // Region->isPopulatingFreeList == false because all the new populated
905 // blocks were used up right after the refillment. Therefore, we have to
906 // check if someone is still populating the freelist.
907 ScopedLock
FL(Region
->FLLock
);
908 if (LIKELY(B
= popBatchImpl(C
, ClassId
, Region
)))
911 if (!Region
->isPopulatingFreeList
)
914 // Now the freelist is empty and someone's doing the refillment. We will
915 // wait until anyone refills the freelist or someone finishes doing
916 // `populateFreeListAndPopBatch()`. The refillment can be done by
917 // `populateFreeListAndPopBatch()`, `pushBlocks()`,
918 // `pushBatchClassBlocks()` and `mergeGroupsToReleaseBack()`.
919 Region
->FLLockCV
.wait(Region
->FLLock
);
921 if (LIKELY(B
= popBatchImpl(C
, ClassId
, Region
)))
928 // Pop one TransferBatch from a BatchGroup. The BatchGroup with the smallest
929 // group id will be considered first.
931 // The region mutex needs to be held while calling this method.
932 TransferBatchT
*popBatchImpl(CacheT
*C
, uptr ClassId
, RegionInfo
*Region
)
933 REQUIRES(Region
->FLLock
) {
934 if (Region
->FreeListInfo
.BlockList
.empty())
937 SinglyLinkedList
<TransferBatchT
> &Batches
=
938 Region
->FreeListInfo
.BlockList
.front()->Batches
;
940 if (Batches
.empty()) {
941 DCHECK_EQ(ClassId
, SizeClassMap::BatchClassId
);
942 BatchGroupT
*BG
= Region
->FreeListInfo
.BlockList
.front();
943 Region
->FreeListInfo
.BlockList
.pop_front();
945 // Block used by `BatchGroup` is from BatchClassId. Turn the block into
946 // `TransferBatch` with single block.
947 TransferBatchT
*TB
= reinterpret_cast<TransferBatchT
*>(BG
);
950 compactPtr(SizeClassMap::BatchClassId
, reinterpret_cast<uptr
>(TB
)));
951 Region
->FreeListInfo
.PoppedBlocks
+= 1;
955 TransferBatchT
*B
= Batches
.front();
957 DCHECK_NE(B
, nullptr);
958 DCHECK_GT(B
->getCount(), 0U);
960 if (Batches
.empty()) {
961 BatchGroupT
*BG
= Region
->FreeListInfo
.BlockList
.front();
962 Region
->FreeListInfo
.BlockList
.pop_front();
964 // We don't keep BatchGroup with zero blocks to avoid empty-checking while
965 // allocating. Note that block used by constructing BatchGroup is recorded
966 // as free blocks in the last element of BatchGroup::Batches. Which means,
967 // once we pop the last TransferBatch, the block is implicitly
969 if (ClassId
!= SizeClassMap::BatchClassId
)
970 C
->deallocate(SizeClassMap::BatchClassId
, BG
);
973 Region
->FreeListInfo
.PoppedBlocks
+= B
->getCount();
978 // Refill the freelist and return one batch.
979 NOINLINE TransferBatchT
*populateFreeListAndPopBatch(CacheT
*C
, uptr ClassId
,
981 REQUIRES(Region
->MMLock
) EXCLUDES(Region
->FLLock
) {
982 const uptr Size
= getSizeByClassId(ClassId
);
983 const u16 MaxCount
= CacheT::getMaxCached(Size
);
985 const uptr RegionBeg
= Region
->RegionBeg
;
986 const uptr MappedUser
= Region
->MemMapInfo
.MappedUser
;
987 const uptr TotalUserBytes
=
988 Region
->MemMapInfo
.AllocatedUser
+ MaxCount
* Size
;
989 // Map more space for blocks, if necessary.
990 if (TotalUserBytes
> MappedUser
) {
991 // Do the mmap for the user memory.
993 roundUp(TotalUserBytes
- MappedUser
, MapSizeIncrement
);
994 const uptr RegionBase
= RegionBeg
- getRegionBaseByClassId(ClassId
);
995 if (UNLIKELY(RegionBase
+ MappedUser
+ MapSize
> RegionSize
)) {
996 Region
->Exhausted
= true;
1000 if (UNLIKELY(!Region
->MemMapInfo
.MemMap
.remap(
1001 RegionBeg
+ MappedUser
, MapSize
, "scudo:primary",
1002 MAP_ALLOWNOMEM
| MAP_RESIZABLE
|
1003 (useMemoryTagging
<Config
>(Options
.load()) ? MAP_MEMTAG
1007 Region
->MemMapInfo
.MappedUser
+= MapSize
;
1008 C
->getStats().add(StatMapped
, MapSize
);
1011 const u32 NumberOfBlocks
=
1012 Min(MaxNumBatches
* MaxCount
,
1013 static_cast<u32
>((Region
->MemMapInfo
.MappedUser
-
1014 Region
->MemMapInfo
.AllocatedUser
) /
1016 DCHECK_GT(NumberOfBlocks
, 0);
1018 constexpr u32 ShuffleArraySize
=
1019 MaxNumBatches
* TransferBatchT::MaxNumCached
;
1020 CompactPtrT ShuffleArray
[ShuffleArraySize
];
1021 DCHECK_LE(NumberOfBlocks
, ShuffleArraySize
);
1023 const uptr CompactPtrBase
= getCompactPtrBaseByClassId(ClassId
);
1024 uptr P
= RegionBeg
+ Region
->MemMapInfo
.AllocatedUser
;
1025 for (u32 I
= 0; I
< NumberOfBlocks
; I
++, P
+= Size
)
1026 ShuffleArray
[I
] = compactPtrInternal(CompactPtrBase
, P
);
1028 ScopedLock
L(Region
->FLLock
);
1030 if (ClassId
!= SizeClassMap::BatchClassId
) {
1032 uptr CurGroup
= compactPtrGroup(ShuffleArray
[0]);
1033 for (u32 I
= 1; I
< NumberOfBlocks
; I
++) {
1034 if (UNLIKELY(compactPtrGroup(ShuffleArray
[I
]) != CurGroup
)) {
1035 shuffle(ShuffleArray
+ I
- N
, N
, &Region
->RandState
);
1036 pushBlocksImpl(C
, ClassId
, Region
, ShuffleArray
+ I
- N
, N
,
1037 /*SameGroup=*/true);
1039 CurGroup
= compactPtrGroup(ShuffleArray
[I
]);
1045 shuffle(ShuffleArray
+ NumberOfBlocks
- N
, N
, &Region
->RandState
);
1046 pushBlocksImpl(C
, ClassId
, Region
, &ShuffleArray
[NumberOfBlocks
- N
], N
,
1047 /*SameGroup=*/true);
1049 pushBatchClassBlocks(Region
, ShuffleArray
, NumberOfBlocks
);
1052 TransferBatchT
*B
= popBatchImpl(C
, ClassId
, Region
);
1053 DCHECK_NE(B
, nullptr);
1055 // Note that `PushedBlocks` and `PoppedBlocks` are supposed to only record
1056 // the requests from `PushBlocks` and `PopBatch` which are external
1057 // interfaces. `populateFreeListAndPopBatch` is the internal interface so we
1058 // should set the values back to avoid incorrectly setting the stats.
1059 Region
->FreeListInfo
.PushedBlocks
-= NumberOfBlocks
;
1061 const uptr AllocatedUser
= Size
* NumberOfBlocks
;
1062 C
->getStats().add(StatFree
, AllocatedUser
);
1063 Region
->MemMapInfo
.AllocatedUser
+= AllocatedUser
;
1068 void getStats(ScopedString
*Str
, uptr ClassId
, RegionInfo
*Region
)
1069 REQUIRES(Region
->MMLock
, Region
->FLLock
) {
1070 if (Region
->MemMapInfo
.MappedUser
== 0)
1072 const uptr BlockSize
= getSizeByClassId(ClassId
);
1073 const uptr InUseBlocks
=
1074 Region
->FreeListInfo
.PoppedBlocks
- Region
->FreeListInfo
.PushedBlocks
;
1075 const uptr BytesInFreeList
=
1076 Region
->MemMapInfo
.AllocatedUser
- InUseBlocks
* BlockSize
;
1077 uptr RegionPushedBytesDelta
= 0;
1078 if (BytesInFreeList
>=
1079 Region
->ReleaseInfo
.BytesInFreeListAtLastCheckpoint
) {
1080 RegionPushedBytesDelta
=
1081 BytesInFreeList
- Region
->ReleaseInfo
.BytesInFreeListAtLastCheckpoint
;
1083 const uptr TotalChunks
= Region
->MemMapInfo
.AllocatedUser
/ BlockSize
;
1085 "%s %02zu (%6zu): mapped: %6zuK popped: %7zu pushed: %7zu "
1086 "inuse: %6zu total: %6zu releases: %6zu last "
1087 "released: %6zuK latest pushed bytes: %6zuK region: 0x%zx (0x%zx)\n",
1088 Region
->Exhausted
? "E" : " ", ClassId
, getSizeByClassId(ClassId
),
1089 Region
->MemMapInfo
.MappedUser
>> 10, Region
->FreeListInfo
.PoppedBlocks
,
1090 Region
->FreeListInfo
.PushedBlocks
, InUseBlocks
, TotalChunks
,
1091 Region
->ReleaseInfo
.RangesReleased
,
1092 Region
->ReleaseInfo
.LastReleasedBytes
>> 10,
1093 RegionPushedBytesDelta
>> 10, Region
->RegionBeg
,
1094 getRegionBaseByClassId(ClassId
));
1097 void getRegionFragmentationInfo(RegionInfo
*Region
, uptr ClassId
,
1098 ScopedString
*Str
) REQUIRES(Region
->MMLock
) {
1099 const uptr BlockSize
= getSizeByClassId(ClassId
);
1100 const uptr AllocatedUserEnd
=
1101 Region
->MemMapInfo
.AllocatedUser
+ Region
->RegionBeg
;
1103 SinglyLinkedList
<BatchGroupT
> GroupsToRelease
;
1105 ScopedLock
L(Region
->FLLock
);
1106 GroupsToRelease
= Region
->FreeListInfo
.BlockList
;
1107 Region
->FreeListInfo
.BlockList
.clear();
1110 FragmentationRecorder Recorder
;
1111 if (!GroupsToRelease
.empty()) {
1112 PageReleaseContext Context
=
1113 markFreeBlocks(Region
, BlockSize
, AllocatedUserEnd
,
1114 getCompactPtrBaseByClassId(ClassId
), GroupsToRelease
);
1115 auto SkipRegion
= [](UNUSED uptr RegionIndex
) { return false; };
1116 releaseFreeMemoryToOS(Context
, Recorder
, SkipRegion
);
1118 mergeGroupsToReleaseBack(Region
, GroupsToRelease
);
1121 ScopedLock
L(Region
->FLLock
);
1122 const uptr PageSize
= getPageSizeCached();
1123 const uptr TotalBlocks
= Region
->MemMapInfo
.AllocatedUser
/ BlockSize
;
1124 const uptr InUseBlocks
=
1125 Region
->FreeListInfo
.PoppedBlocks
- Region
->FreeListInfo
.PushedBlocks
;
1126 const uptr AllocatedPagesCount
=
1127 roundUp(Region
->MemMapInfo
.AllocatedUser
, PageSize
) / PageSize
;
1128 DCHECK_GE(AllocatedPagesCount
, Recorder
.getReleasedPagesCount());
1129 const uptr InUsePages
=
1130 AllocatedPagesCount
- Recorder
.getReleasedPagesCount();
1131 const uptr InUseBytes
= InUsePages
* PageSize
;
1135 computePercentage(BlockSize
* InUseBlocks
, InUsePages
* PageSize
, &Integral
,
1137 Str
->append(" %02zu (%6zu): inuse/total blocks: %6zu/%6zu inuse/total "
1138 "pages: %6zu/%6zu inuse bytes: %6zuK util: %3zu.%02zu%%\n",
1139 ClassId
, BlockSize
, InUseBlocks
, TotalBlocks
, InUsePages
,
1140 AllocatedPagesCount
, InUseBytes
>> 10, Integral
, Fractional
);
1143 NOINLINE uptr
releaseToOSMaybe(RegionInfo
*Region
, uptr ClassId
,
1144 ReleaseToOS ReleaseType
= ReleaseToOS::Normal
)
1145 REQUIRES(Region
->MMLock
) EXCLUDES(Region
->FLLock
) {
1146 const uptr BlockSize
= getSizeByClassId(ClassId
);
1147 uptr BytesInFreeList
;
1148 const uptr AllocatedUserEnd
=
1149 Region
->MemMapInfo
.AllocatedUser
+ Region
->RegionBeg
;
1150 SinglyLinkedList
<BatchGroupT
> GroupsToRelease
;
1153 ScopedLock
L(Region
->FLLock
);
1155 BytesInFreeList
= Region
->MemMapInfo
.AllocatedUser
-
1156 (Region
->FreeListInfo
.PoppedBlocks
-
1157 Region
->FreeListInfo
.PushedBlocks
) *
1159 if (UNLIKELY(BytesInFreeList
== 0))
1162 // ==================================================================== //
1163 // 1. Check if we have enough free blocks and if it's worth doing a page
1165 // ==================================================================== //
1166 if (ReleaseType
!= ReleaseToOS::ForceAll
&&
1167 !hasChanceToReleasePages(Region
, BlockSize
, BytesInFreeList
,
1172 // ==================================================================== //
1173 // 2. Determine which groups can release the pages. Use a heuristic to
1174 // gather groups that are candidates for doing a release.
1175 // ==================================================================== //
1176 if (ReleaseType
== ReleaseToOS::ForceAll
) {
1177 GroupsToRelease
= Region
->FreeListInfo
.BlockList
;
1178 Region
->FreeListInfo
.BlockList
.clear();
1181 collectGroupsToRelease(Region
, BlockSize
, AllocatedUserEnd
,
1182 getCompactPtrBaseByClassId(ClassId
));
1184 if (GroupsToRelease
.empty())
1188 // Note that we have extracted the `GroupsToRelease` from region freelist.
1189 // It's safe to let pushBlocks()/popBatches() access the remaining region
1190 // freelist. In the steps 3 and 4, we will temporarily release the FLLock
1191 // and lock it again before step 5.
1193 // ==================================================================== //
1194 // 3. Mark the free blocks in `GroupsToRelease` in the `PageReleaseContext`.
1195 // Then we can tell which pages are in-use by querying
1196 // `PageReleaseContext`.
1197 // ==================================================================== //
1198 PageReleaseContext Context
=
1199 markFreeBlocks(Region
, BlockSize
, AllocatedUserEnd
,
1200 getCompactPtrBaseByClassId(ClassId
), GroupsToRelease
);
1201 if (UNLIKELY(!Context
.hasBlockMarked())) {
1202 mergeGroupsToReleaseBack(Region
, GroupsToRelease
);
1206 // ==================================================================== //
1207 // 4. Release the unused physical pages back to the OS.
1208 // ==================================================================== //
1209 RegionReleaseRecorder
<MemMapT
> Recorder(&Region
->MemMapInfo
.MemMap
,
1211 Context
.getReleaseOffset());
1212 auto SkipRegion
= [](UNUSED uptr RegionIndex
) { return false; };
1213 releaseFreeMemoryToOS(Context
, Recorder
, SkipRegion
);
1214 if (Recorder
.getReleasedRangesCount() > 0) {
1215 Region
->ReleaseInfo
.BytesInFreeListAtLastCheckpoint
= BytesInFreeList
;
1216 Region
->ReleaseInfo
.RangesReleased
+= Recorder
.getReleasedRangesCount();
1217 Region
->ReleaseInfo
.LastReleasedBytes
= Recorder
.getReleasedBytes();
1219 Region
->ReleaseInfo
.LastReleaseAtNs
= getMonotonicTimeFast();
1221 // ====================================================================== //
1222 // 5. Merge the `GroupsToRelease` back to the freelist.
1223 // ====================================================================== //
1224 mergeGroupsToReleaseBack(Region
, GroupsToRelease
);
1226 return Recorder
.getReleasedBytes();
1229 bool hasChanceToReleasePages(RegionInfo
*Region
, uptr BlockSize
,
1230 uptr BytesInFreeList
, ReleaseToOS ReleaseType
)
1231 REQUIRES(Region
->MMLock
, Region
->FLLock
) {
1232 DCHECK_GE(Region
->FreeListInfo
.PoppedBlocks
,
1233 Region
->FreeListInfo
.PushedBlocks
);
1234 const uptr PageSize
= getPageSizeCached();
1236 // Always update `BytesInFreeListAtLastCheckpoint` with the smallest value
1237 // so that we won't underestimate the releasable pages. For example, the
1238 // following is the region usage,
1240 // BytesInFreeListAtLastCheckpoint AllocatedUser
1242 // |--------------------------------------->
1244 // BytesInFreeList ReleaseThreshold
1246 // In general, if we have collected enough bytes and the amount of free
1247 // bytes meets the ReleaseThreshold, we will try to do page release. If we
1248 // don't update `BytesInFreeListAtLastCheckpoint` when the current
1249 // `BytesInFreeList` is smaller, we may take longer time to wait for enough
1250 // freed blocks because we miss the bytes between
1251 // (BytesInFreeListAtLastCheckpoint - BytesInFreeList).
1252 if (BytesInFreeList
<=
1253 Region
->ReleaseInfo
.BytesInFreeListAtLastCheckpoint
) {
1254 Region
->ReleaseInfo
.BytesInFreeListAtLastCheckpoint
= BytesInFreeList
;
1257 const uptr RegionPushedBytesDelta
=
1258 BytesInFreeList
- Region
->ReleaseInfo
.BytesInFreeListAtLastCheckpoint
;
1259 if (RegionPushedBytesDelta
< PageSize
)
1262 // Releasing smaller blocks is expensive, so we want to make sure that a
1263 // significant amount of bytes are free, and that there has been a good
1264 // amount of batches pushed to the freelist before attempting to release.
1265 if (isSmallBlock(BlockSize
) && ReleaseType
== ReleaseToOS::Normal
)
1266 if (RegionPushedBytesDelta
< Region
->TryReleaseThreshold
)
1269 if (ReleaseType
== ReleaseToOS::Normal
) {
1270 const s32 IntervalMs
= atomic_load_relaxed(&ReleaseToOsIntervalMs
);
1274 // The constant 8 here is selected from profiling some apps and the number
1275 // of unreleased pages in the large size classes is around 16 pages or
1276 // more. Choose half of it as a heuristic and which also avoids page
1277 // release every time for every pushBlocks() attempt by large blocks.
1278 const bool ByPassReleaseInterval
=
1279 isLargeBlock(BlockSize
) && RegionPushedBytesDelta
> 8 * PageSize
;
1280 if (!ByPassReleaseInterval
) {
1281 if (Region
->ReleaseInfo
.LastReleaseAtNs
+
1282 static_cast<u64
>(IntervalMs
) * 1000000 >
1283 getMonotonicTimeFast()) {
1284 // Memory was returned recently.
1288 } // if (ReleaseType == ReleaseToOS::Normal)
1293 SinglyLinkedList
<BatchGroupT
>
1294 collectGroupsToRelease(RegionInfo
*Region
, const uptr BlockSize
,
1295 const uptr AllocatedUserEnd
, const uptr CompactPtrBase
)
1296 REQUIRES(Region
->MMLock
, Region
->FLLock
) {
1297 const uptr GroupSize
= (1UL << GroupSizeLog
);
1298 const uptr PageSize
= getPageSizeCached();
1299 SinglyLinkedList
<BatchGroupT
> GroupsToRelease
;
1301 // We are examining each group and will take the minimum distance to the
1302 // release threshold as the next Region::TryReleaseThreshold(). Note that if
1303 // the size of free blocks has reached the release threshold, the distance
1304 // to the next release will be PageSize * SmallerBlockReleasePageDelta. See
1305 // the comment on `SmallerBlockReleasePageDelta` for more details.
1306 uptr MinDistToThreshold
= GroupSize
;
1308 for (BatchGroupT
*BG
= Region
->FreeListInfo
.BlockList
.front(),
1311 // Group boundary is always GroupSize-aligned from CompactPtr base. The
1312 // layout of memory groups is like,
1315 // #1 CompactPtrGroupBase #2 CompactPtrGroupBase ...
1318 // +-----------------------+-----------------------+
1320 // --- GroupSize --- --- GroupSize ---
1322 // After decompacting the CompactPtrGroupBase, we expect the alignment
1323 // property is held as well.
1324 const uptr BatchGroupBase
=
1325 decompactGroupBase(CompactPtrBase
, BG
->CompactPtrGroupBase
);
1326 DCHECK_LE(Region
->RegionBeg
, BatchGroupBase
);
1327 DCHECK_GE(AllocatedUserEnd
, BatchGroupBase
);
1328 DCHECK_EQ((Region
->RegionBeg
- BatchGroupBase
) % GroupSize
, 0U);
1329 // TransferBatches are pushed in front of BG.Batches. The first one may
1330 // not have all caches used.
1331 const uptr NumBlocks
= (BG
->Batches
.size() - 1) * BG
->MaxCachedPerBatch
+
1332 BG
->Batches
.front()->getCount();
1333 const uptr BytesInBG
= NumBlocks
* BlockSize
;
1335 if (BytesInBG
<= BG
->BytesInBGAtLastCheckpoint
) {
1336 BG
->BytesInBGAtLastCheckpoint
= BytesInBG
;
1342 const uptr PushedBytesDelta
= BG
->BytesInBGAtLastCheckpoint
- BytesInBG
;
1344 // Given the randomness property, we try to release the pages only if the
1345 // bytes used by free blocks exceed certain proportion of group size. Note
1346 // that this heuristic only applies when all the spaces in a BatchGroup
1348 if (isSmallBlock(BlockSize
)) {
1349 const uptr BatchGroupEnd
= BatchGroupBase
+ GroupSize
;
1350 const uptr AllocatedGroupSize
= AllocatedUserEnd
>= BatchGroupEnd
1352 : AllocatedUserEnd
- BatchGroupBase
;
1353 const uptr ReleaseThreshold
=
1354 (AllocatedGroupSize
* (100 - 1U - BlockSize
/ 16U)) / 100U;
1355 const bool HighDensity
= BytesInBG
>= ReleaseThreshold
;
1356 const bool MayHaveReleasedAll
= NumBlocks
>= (GroupSize
/ BlockSize
);
1357 // If all blocks in the group are released, we will do range marking
1358 // which is fast. Otherwise, we will wait until we have accumulated
1359 // a certain amount of free memory.
1360 const bool ReachReleaseDelta
=
1363 : PushedBytesDelta
>= PageSize
* SmallerBlockReleasePageDelta
;
1366 DCHECK_LE(BytesInBG
, ReleaseThreshold
);
1367 // The following is the usage of a memroy group,
1369 // BytesInBG ReleaseThreshold
1371 // +---+---------------------------+-----+
1373 // +---+---------------------------+-----+
1375 // PushedBytesDelta GroupEnd
1376 MinDistToThreshold
=
1377 Min(MinDistToThreshold
,
1378 ReleaseThreshold
- BytesInBG
+ PushedBytesDelta
);
1380 // If it reaches high density at this round, the next time we will try
1381 // to release is based on SmallerBlockReleasePageDelta
1382 MinDistToThreshold
=
1383 Min(MinDistToThreshold
, PageSize
* SmallerBlockReleasePageDelta
);
1386 if (!HighDensity
|| !ReachReleaseDelta
) {
1393 // If `BG` is the first BatchGroupT in the list, we only need to advance
1394 // `BG` and call FreeListInfo.BlockList::pop_front(). No update is needed
1402 // |X | -> | | -> ...
1405 // Otherwise, `Prev` will be used to extract the `Cur` from the
1406 // `FreeListInfo.BlockList`.
1413 // | | -> |X | -> | | -> ...
1416 // After FreeListInfo.BlockList::extract(),
1422 // | |-+ |X | +->| | -> ...
1423 // +--+ | +--+ | +--+
1426 // Note that we need to advance before pushing this BatchGroup to
1427 // GroupsToRelease because it's a destructive operation.
1429 BatchGroupT
*Cur
= BG
;
1432 // Ideally, we may want to update this only after successful release.
1433 // However, for smaller blocks, each block marking is a costly operation.
1434 // Therefore, we update it earlier.
1435 // TODO: Consider updating this after releasing pages if `ReleaseRecorder`
1436 // can tell the released bytes in each group.
1437 Cur
->BytesInBGAtLastCheckpoint
= BytesInBG
;
1439 if (Prev
!= nullptr)
1440 Region
->FreeListInfo
.BlockList
.extract(Prev
, Cur
);
1442 Region
->FreeListInfo
.BlockList
.pop_front();
1443 GroupsToRelease
.push_back(Cur
);
1446 // Only small blocks have the adaptive `TryReleaseThreshold`.
1447 if (isSmallBlock(BlockSize
)) {
1448 // If the MinDistToThreshold is not updated, that means each memory group
1449 // may have only pushed less than a page size. In that case, just set it
1451 if (MinDistToThreshold
== GroupSize
)
1452 MinDistToThreshold
= PageSize
* SmallerBlockReleasePageDelta
;
1453 Region
->TryReleaseThreshold
= MinDistToThreshold
;
1456 return GroupsToRelease
;
1460 markFreeBlocks(RegionInfo
*Region
, const uptr BlockSize
,
1461 const uptr AllocatedUserEnd
, const uptr CompactPtrBase
,
1462 SinglyLinkedList
<BatchGroupT
> &GroupsToRelease
)
1463 REQUIRES(Region
->MMLock
) EXCLUDES(Region
->FLLock
) {
1464 const uptr GroupSize
= (1UL << GroupSizeLog
);
1465 auto DecompactPtr
= [CompactPtrBase
](CompactPtrT CompactPtr
) {
1466 return decompactPtrInternal(CompactPtrBase
, CompactPtr
);
1469 const uptr ReleaseBase
= decompactGroupBase(
1470 CompactPtrBase
, GroupsToRelease
.front()->CompactPtrGroupBase
);
1471 const uptr LastGroupEnd
=
1472 Min(decompactGroupBase(CompactPtrBase
,
1473 GroupsToRelease
.back()->CompactPtrGroupBase
) +
1476 // The last block may straddle the group boundary. Rounding up to BlockSize
1477 // to get the exact range.
1478 const uptr ReleaseEnd
=
1479 roundUpSlow(LastGroupEnd
- Region
->RegionBeg
, BlockSize
) +
1481 const uptr ReleaseRangeSize
= ReleaseEnd
- ReleaseBase
;
1482 const uptr ReleaseOffset
= ReleaseBase
- Region
->RegionBeg
;
1484 PageReleaseContext
Context(BlockSize
, /*NumberOfRegions=*/1U,
1485 ReleaseRangeSize
, ReleaseOffset
);
1486 // We may not be able to do the page release in a rare case that we may
1487 // fail on PageMap allocation.
1488 if (UNLIKELY(!Context
.ensurePageMapAllocated()))
1491 for (BatchGroupT
&BG
: GroupsToRelease
) {
1492 const uptr BatchGroupBase
=
1493 decompactGroupBase(CompactPtrBase
, BG
.CompactPtrGroupBase
);
1494 const uptr BatchGroupEnd
= BatchGroupBase
+ GroupSize
;
1495 const uptr AllocatedGroupSize
= AllocatedUserEnd
>= BatchGroupEnd
1497 : AllocatedUserEnd
- BatchGroupBase
;
1498 const uptr BatchGroupUsedEnd
= BatchGroupBase
+ AllocatedGroupSize
;
1499 const bool MayContainLastBlockInRegion
=
1500 BatchGroupUsedEnd
== AllocatedUserEnd
;
1501 const bool BlockAlignedWithUsedEnd
=
1502 (BatchGroupUsedEnd
- Region
->RegionBeg
) % BlockSize
== 0;
1504 uptr MaxContainedBlocks
= AllocatedGroupSize
/ BlockSize
;
1505 if (!BlockAlignedWithUsedEnd
)
1506 ++MaxContainedBlocks
;
1508 const uptr NumBlocks
= (BG
.Batches
.size() - 1) * BG
.MaxCachedPerBatch
+
1509 BG
.Batches
.front()->getCount();
1511 if (NumBlocks
== MaxContainedBlocks
) {
1512 for (const auto &It
: BG
.Batches
) {
1513 if (&It
!= BG
.Batches
.front())
1514 DCHECK_EQ(It
.getCount(), BG
.MaxCachedPerBatch
);
1515 for (u16 I
= 0; I
< It
.getCount(); ++I
)
1516 DCHECK_EQ(compactPtrGroup(It
.get(I
)), BG
.CompactPtrGroupBase
);
1519 Context
.markRangeAsAllCounted(BatchGroupBase
, BatchGroupUsedEnd
,
1520 Region
->RegionBeg
, /*RegionIndex=*/0,
1521 Region
->MemMapInfo
.AllocatedUser
);
1523 DCHECK_LT(NumBlocks
, MaxContainedBlocks
);
1524 // Note that we don't always visit blocks in each BatchGroup so that we
1525 // may miss the chance of releasing certain pages that cross
1527 Context
.markFreeBlocksInRegion(
1528 BG
.Batches
, DecompactPtr
, Region
->RegionBeg
, /*RegionIndex=*/0,
1529 Region
->MemMapInfo
.AllocatedUser
, MayContainLastBlockInRegion
);
1533 DCHECK(Context
.hasBlockMarked());
1538 void mergeGroupsToReleaseBack(RegionInfo
*Region
,
1539 SinglyLinkedList
<BatchGroupT
> &GroupsToRelease
)
1540 REQUIRES(Region
->MMLock
) EXCLUDES(Region
->FLLock
) {
1541 ScopedLock
L(Region
->FLLock
);
1543 // After merging two freelists, we may have redundant `BatchGroup`s that
1544 // need to be recycled. The number of unused `BatchGroup`s is expected to be
1545 // small. Pick a constant which is inferred from real programs.
1546 constexpr uptr MaxUnusedSize
= 8;
1547 CompactPtrT Blocks
[MaxUnusedSize
];
1549 RegionInfo
*BatchClassRegion
= getRegionInfo(SizeClassMap::BatchClassId
);
1550 // We can't call pushBatchClassBlocks() to recycle the unused `BatchGroup`s
1551 // when we are manipulating the freelist of `BatchClassRegion`. Instead, we
1552 // should just push it back to the freelist when we merge two `BatchGroup`s.
1553 // This logic hasn't been implemented because we haven't supported releasing
1554 // pages in `BatchClassRegion`.
1555 DCHECK_NE(BatchClassRegion
, Region
);
1557 // Merge GroupsToRelease back to the Region::FreeListInfo.BlockList. Note
1558 // that both `Region->FreeListInfo.BlockList` and `GroupsToRelease` are
1560 for (BatchGroupT
*BG
= Region
->FreeListInfo
.BlockList
.front(),
1563 if (BG
== nullptr || GroupsToRelease
.empty()) {
1564 if (!GroupsToRelease
.empty())
1565 Region
->FreeListInfo
.BlockList
.append_back(&GroupsToRelease
);
1569 DCHECK(!BG
->Batches
.empty());
1571 if (BG
->CompactPtrGroupBase
<
1572 GroupsToRelease
.front()->CompactPtrGroupBase
) {
1578 BatchGroupT
*Cur
= GroupsToRelease
.front();
1579 TransferBatchT
*UnusedTransferBatch
= nullptr;
1580 GroupsToRelease
.pop_front();
1582 if (BG
->CompactPtrGroupBase
== Cur
->CompactPtrGroupBase
) {
1583 BG
->PushedBlocks
+= Cur
->PushedBlocks
;
1584 // We have updated `BatchGroup::BytesInBGAtLastCheckpoint` while
1585 // collecting the `GroupsToRelease`.
1586 BG
->BytesInBGAtLastCheckpoint
= Cur
->BytesInBGAtLastCheckpoint
;
1587 const uptr MaxCachedPerBatch
= BG
->MaxCachedPerBatch
;
1589 // Note that the first TransferBatches in both `Batches` may not be
1590 // full and only the first TransferBatch can have non-full blocks. Thus
1591 // we have to merge them before appending one to another.
1592 if (Cur
->Batches
.front()->getCount() == MaxCachedPerBatch
) {
1593 BG
->Batches
.append_back(&Cur
->Batches
);
1595 TransferBatchT
*NonFullBatch
= Cur
->Batches
.front();
1596 Cur
->Batches
.pop_front();
1597 const u16 NonFullBatchCount
= NonFullBatch
->getCount();
1598 // The remaining Batches in `Cur` are full.
1599 BG
->Batches
.append_back(&Cur
->Batches
);
1601 if (BG
->Batches
.front()->getCount() == MaxCachedPerBatch
) {
1602 // Only 1 non-full TransferBatch, push it to the front.
1603 BG
->Batches
.push_front(NonFullBatch
);
1605 const u16 NumBlocksToMove
= static_cast<u16
>(
1606 Min(static_cast<u16
>(MaxCachedPerBatch
-
1607 BG
->Batches
.front()->getCount()),
1608 NonFullBatchCount
));
1609 BG
->Batches
.front()->appendFromTransferBatch(NonFullBatch
,
1611 if (NonFullBatch
->isEmpty())
1612 UnusedTransferBatch
= NonFullBatch
;
1614 BG
->Batches
.push_front(NonFullBatch
);
1618 const u32 NeededSlots
= UnusedTransferBatch
== nullptr ? 1U : 2U;
1619 if (UNLIKELY(Idx
+ NeededSlots
> MaxUnusedSize
)) {
1620 ScopedLock
L(BatchClassRegion
->FLLock
);
1621 pushBatchClassBlocks(BatchClassRegion
, Blocks
, Idx
);
1622 if (conditionVariableEnabled())
1623 BatchClassRegion
->FLLockCV
.notifyAll(BatchClassRegion
->FLLock
);
1627 compactPtr(SizeClassMap::BatchClassId
, reinterpret_cast<uptr
>(Cur
));
1628 if (UnusedTransferBatch
) {
1630 compactPtr(SizeClassMap::BatchClassId
,
1631 reinterpret_cast<uptr
>(UnusedTransferBatch
));
1638 // At here, the `BG` is the first BatchGroup with CompactPtrGroupBase
1639 // larger than the first element in `GroupsToRelease`. We need to insert
1640 // `GroupsToRelease::front()` (which is `Cur` below) before `BG`.
1642 // 1. If `Prev` is nullptr, we simply push `Cur` to the front of
1643 // FreeListInfo.BlockList.
1644 // 2. Otherwise, use `insert()` which inserts an element next to `Prev`.
1646 // Afterwards, we don't need to advance `BG` because the order between
1647 // `BG` and the new `GroupsToRelease::front()` hasn't been checked.
1648 if (Prev
== nullptr)
1649 Region
->FreeListInfo
.BlockList
.push_front(Cur
);
1651 Region
->FreeListInfo
.BlockList
.insert(Prev
, Cur
);
1652 DCHECK_EQ(Cur
->Next
, BG
);
1657 ScopedLock
L(BatchClassRegion
->FLLock
);
1658 pushBatchClassBlocks(BatchClassRegion
, Blocks
, Idx
);
1659 if (conditionVariableEnabled())
1660 BatchClassRegion
->FLLockCV
.notifyAll(BatchClassRegion
->FLLock
);
1664 BatchGroupT
*Prev
= Region
->FreeListInfo
.BlockList
.front();
1665 for (BatchGroupT
*Cur
= Prev
->Next
; Cur
!= nullptr;
1666 Prev
= Cur
, Cur
= Cur
->Next
) {
1667 CHECK_LT(Prev
->CompactPtrGroupBase
, Cur
->CompactPtrGroupBase
);
1671 if (conditionVariableEnabled())
1672 Region
->FLLockCV
.notifyAll(Region
->FLLock
);
1675 // TODO: `PrimaryBase` can be obtained from ReservedMemory. This needs to be
1677 uptr PrimaryBase
= 0;
1678 ReservedMemoryT ReservedMemory
= {};
1679 // The minimum size of pushed blocks that we will try to release the pages in
1681 uptr SmallerBlockReleasePageDelta
= 0;
1682 atomic_s32 ReleaseToOsIntervalMs
= {};
1683 alignas(SCUDO_CACHE_LINE_SIZE
) RegionInfo RegionInfoArray
[NumClasses
];
1686 } // namespace scudo
1688 #endif // SCUDO_PRIMARY64_H_