xref: /freebsd/contrib/llvm-project/compiler-rt/lib/scudo/standalone/primary64.h (revision 59c8e88e72633afbc47a4ace0d2170d00d51f7dc)
1 //===-- primary64.h ---------------------------------------------*- C++ -*-===//
2 //
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
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #ifndef SCUDO_PRIMARY64_H_
10 #define SCUDO_PRIMARY64_H_
11 
12 #include "bytemap.h"
13 #include "common.h"
14 #include "list.h"
15 #include "local_cache.h"
16 #include "mem_map.h"
17 #include "memtag.h"
18 #include "options.h"
19 #include "release.h"
20 #include "stats.h"
21 #include "string_utils.h"
22 #include "thread_annotations.h"
23 
24 namespace scudo {
25 
26 // SizeClassAllocator64 is an allocator tuned for 64-bit address space.
27 //
28 // It starts by reserving NumClasses * 2^RegionSizeLog bytes, equally divided in
29 // Regions, specific to each size class. Note that the base of that mapping is
30 // random (based to the platform specific map() capabilities). If
31 // PrimaryEnableRandomOffset is set, each Region actually starts at a random
32 // offset from its base.
33 //
34 // Regions are mapped incrementally on demand to fulfill allocation requests,
35 // those mappings being split into equally sized Blocks based on the size class
36 // they belong to. The Blocks created are shuffled to prevent predictable
37 // address patterns (the predictability increases with the size of the Blocks).
38 //
39 // The 1st Region (for size class 0) holds the TransferBatches. This is a
40 // structure used to transfer arrays of available pointers from the class size
41 // freelist to the thread specific freelist, and back.
42 //
43 // The memory used by this allocator is never unmapped, but can be partially
44 // released if the platform allows for it.
45 
46 template <typename Config> class SizeClassAllocator64 {
47 public:
48   typedef typename Config::Primary::CompactPtrT CompactPtrT;
49   typedef typename Config::Primary::SizeClassMap SizeClassMap;
50   static const uptr CompactPtrScale = Config::Primary::CompactPtrScale;
51   static const uptr GroupSizeLog = Config::Primary::GroupSizeLog;
52   static const uptr GroupScale = GroupSizeLog - CompactPtrScale;
53   typedef SizeClassAllocator64<Config> ThisT;
54   typedef SizeClassAllocatorLocalCache<ThisT> CacheT;
55   typedef typename CacheT::TransferBatch TransferBatch;
56   typedef typename CacheT::BatchGroup BatchGroup;
57 
58   static uptr getSizeByClassId(uptr ClassId) {
59     return (ClassId == SizeClassMap::BatchClassId)
60                ? roundUp(sizeof(TransferBatch), 1U << CompactPtrScale)
61                : SizeClassMap::getSizeByClassId(ClassId);
62   }
63 
64   static bool canAllocate(uptr Size) { return Size <= SizeClassMap::MaxSize; }
65 
66   void init(s32 ReleaseToOsInterval) NO_THREAD_SAFETY_ANALYSIS {
67     DCHECK(isAligned(reinterpret_cast<uptr>(this), alignof(ThisT)));
68 
69     const uptr PageSize = getPageSizeCached();
70     const uptr GroupSize = (1U << GroupSizeLog);
71     const uptr PagesInGroup = GroupSize / PageSize;
72     const uptr MinSizeClass = getSizeByClassId(1);
73     // When trying to release pages back to memory, visiting smaller size
74     // classes is expensive. Therefore, we only try to release smaller size
75     // classes when the amount of free blocks goes over a certain threshold (See
76     // the comment in releaseToOSMaybe() for more details). For example, for
77     // size class 32, we only do the release when the size of free blocks is
78     // greater than 97% of pages in a group. However, this may introduce another
79     // issue that if the number of free blocks is bouncing between 97% ~ 100%.
80     // Which means we may try many page releases but only release very few of
81     // them (less than 3% in a group). Even though we have
82     // `&ReleaseToOsIntervalMs` which slightly reduce the frequency of these
83     // calls but it will be better to have another guard to mitigate this issue.
84     //
85     // Here we add another constraint on the minimum size requirement. The
86     // constraint is determined by the size of in-use blocks in the minimal size
87     // class. Take size class 32 as an example,
88     //
89     //   +-     one memory group      -+
90     //   +----------------------+------+
91     //   |  97% of free blocks  |      |
92     //   +----------------------+------+
93     //                           \    /
94     //                      3% in-use blocks
95     //
96     //   * The release size threshold is 97%.
97     //
98     // The 3% size in a group is about 7 pages. For two consecutive
99     // releaseToOSMaybe(), we require the difference between `PushedBlocks`
100     // should be greater than 7 pages. This mitigates the page releasing
101     // thrashing which is caused by memory usage bouncing around the threshold.
102     // The smallest size class takes longest time to do the page release so we
103     // use its size of in-use blocks as a heuristic.
104     SmallerBlockReleasePageDelta =
105         PagesInGroup * (1 + MinSizeClass / 16U) / 100;
106 
107     // Reserve the space required for the Primary.
108     CHECK(ReservedMemory.create(/*Addr=*/0U, PrimarySize,
109                                 "scudo:primary_reserve"));
110     PrimaryBase = ReservedMemory.getBase();
111     DCHECK_NE(PrimaryBase, 0U);
112 
113     u32 Seed;
114     const u64 Time = getMonotonicTimeFast();
115     if (!getRandom(reinterpret_cast<void *>(&Seed), sizeof(Seed)))
116       Seed = static_cast<u32>(Time ^ (PrimaryBase >> 12));
117 
118     for (uptr I = 0; I < NumClasses; I++) {
119       RegionInfo *Region = getRegionInfo(I);
120       // The actual start of a region is offset by a random number of pages
121       // when PrimaryEnableRandomOffset is set.
122       Region->RegionBeg =
123           (PrimaryBase + (I << Config::Primary::RegionSizeLog)) +
124           (Config::Primary::EnableRandomOffset
125                ? ((getRandomModN(&Seed, 16) + 1) * PageSize)
126                : 0);
127       Region->RandState = getRandomU32(&Seed);
128       // Releasing small blocks is expensive, set a higher threshold to avoid
129       // frequent page releases.
130       if (isSmallBlock(getSizeByClassId(I)))
131         Region->TryReleaseThreshold = PageSize * SmallerBlockReleasePageDelta;
132       else
133         Region->TryReleaseThreshold = PageSize;
134       Region->ReleaseInfo.LastReleaseAtNs = Time;
135 
136       Region->MemMapInfo.MemMap = ReservedMemory.dispatch(
137           PrimaryBase + (I << Config::Primary::RegionSizeLog), RegionSize);
138       CHECK(Region->MemMapInfo.MemMap.isAllocated());
139     }
140     shuffle(RegionInfoArray, NumClasses, &Seed);
141 
142     setOption(Option::ReleaseInterval, static_cast<sptr>(ReleaseToOsInterval));
143   }
144 
145   void unmapTestOnly() NO_THREAD_SAFETY_ANALYSIS {
146     for (uptr I = 0; I < NumClasses; I++) {
147       RegionInfo *Region = getRegionInfo(I);
148       *Region = {};
149     }
150     if (PrimaryBase)
151       ReservedMemory.release();
152     PrimaryBase = 0U;
153   }
154 
155   // When all blocks are freed, it has to be the same size as `AllocatedUser`.
156   void verifyAllBlocksAreReleasedTestOnly() {
157     // `BatchGroup` and `TransferBatch` also use the blocks from BatchClass.
158     uptr BatchClassUsedInFreeLists = 0;
159     for (uptr I = 0; I < NumClasses; I++) {
160       // We have to count BatchClassUsedInFreeLists in other regions first.
161       if (I == SizeClassMap::BatchClassId)
162         continue;
163       RegionInfo *Region = getRegionInfo(I);
164       ScopedLock ML(Region->MMLock);
165       ScopedLock FL(Region->FLLock);
166       const uptr BlockSize = getSizeByClassId(I);
167       uptr TotalBlocks = 0;
168       for (BatchGroup &BG : Region->FreeListInfo.BlockList) {
169         // `BG::Batches` are `TransferBatches`. +1 for `BatchGroup`.
170         BatchClassUsedInFreeLists += BG.Batches.size() + 1;
171         for (const auto &It : BG.Batches)
172           TotalBlocks += It.getCount();
173       }
174 
175       DCHECK_EQ(TotalBlocks, Region->MemMapInfo.AllocatedUser / BlockSize);
176       DCHECK_EQ(Region->FreeListInfo.PushedBlocks,
177                 Region->FreeListInfo.PoppedBlocks);
178     }
179 
180     RegionInfo *Region = getRegionInfo(SizeClassMap::BatchClassId);
181     ScopedLock ML(Region->MMLock);
182     ScopedLock FL(Region->FLLock);
183     const uptr BlockSize = getSizeByClassId(SizeClassMap::BatchClassId);
184     uptr TotalBlocks = 0;
185     for (BatchGroup &BG : Region->FreeListInfo.BlockList) {
186       if (LIKELY(!BG.Batches.empty())) {
187         for (const auto &It : BG.Batches)
188           TotalBlocks += It.getCount();
189       } else {
190         // `BatchGroup` with empty freelist doesn't have `TransferBatch` record
191         // itself.
192         ++TotalBlocks;
193       }
194     }
195     DCHECK_EQ(TotalBlocks + BatchClassUsedInFreeLists,
196               Region->MemMapInfo.AllocatedUser / BlockSize);
197     DCHECK_GE(Region->FreeListInfo.PoppedBlocks,
198               Region->FreeListInfo.PushedBlocks);
199     const uptr BlocksInUse =
200         Region->FreeListInfo.PoppedBlocks - Region->FreeListInfo.PushedBlocks;
201     DCHECK_EQ(BlocksInUse, BatchClassUsedInFreeLists);
202   }
203 
204   TransferBatch *popBatch(CacheT *C, uptr ClassId) {
205     DCHECK_LT(ClassId, NumClasses);
206     RegionInfo *Region = getRegionInfo(ClassId);
207 
208     {
209       ScopedLock L(Region->FLLock);
210       TransferBatch *B = popBatchImpl(C, ClassId, Region);
211       if (LIKELY(B))
212         return B;
213     }
214 
215     bool PrintStats = false;
216     TransferBatch *B = nullptr;
217 
218     while (true) {
219       // When two threads compete for `Region->MMLock`, we only want one of them
220       // does the populateFreeListAndPopBatch(). To avoid both of them doing
221       // that, always check the freelist before mapping new pages.
222       //
223       // TODO(chiahungduan): Use a condition variable so that we don't need to
224       // hold `Region->MMLock` here.
225       ScopedLock ML(Region->MMLock);
226       {
227         ScopedLock FL(Region->FLLock);
228         B = popBatchImpl(C, ClassId, Region);
229         if (LIKELY(B))
230           return B;
231       }
232 
233       const bool RegionIsExhausted = Region->Exhausted;
234       if (!RegionIsExhausted)
235         B = populateFreeListAndPopBatch(C, ClassId, Region);
236       PrintStats = !RegionIsExhausted && Region->Exhausted;
237       break;
238     }
239 
240     // Note that `getStats()` requires locking each region so we can't call it
241     // while locking the Region->Mutex in the above.
242     if (UNLIKELY(PrintStats)) {
243       ScopedString Str;
244       getStats(&Str);
245       Str.append(
246           "Scudo OOM: The process has exhausted %zuM for size class %zu.\n",
247           RegionSize >> 20, getSizeByClassId(ClassId));
248       Str.output();
249 
250       // Theoretically, BatchClass shouldn't be used up. Abort immediately  when
251       // it happens.
252       if (ClassId == SizeClassMap::BatchClassId)
253         reportOutOfBatchClass();
254     }
255 
256     return B;
257   }
258 
259   // Push the array of free blocks to the designated batch group.
260   void pushBlocks(CacheT *C, uptr ClassId, CompactPtrT *Array, u32 Size) {
261     DCHECK_LT(ClassId, NumClasses);
262     DCHECK_GT(Size, 0);
263 
264     RegionInfo *Region = getRegionInfo(ClassId);
265     if (ClassId == SizeClassMap::BatchClassId) {
266       ScopedLock L(Region->FLLock);
267       pushBatchClassBlocks(Region, Array, Size);
268       return;
269     }
270 
271     // TODO(chiahungduan): Consider not doing grouping if the group size is not
272     // greater than the block size with a certain scale.
273 
274     // Sort the blocks so that blocks belonging to the same group can be pushed
275     // together.
276     bool SameGroup = true;
277     for (u32 I = 1; I < Size; ++I) {
278       if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I]))
279         SameGroup = false;
280       CompactPtrT Cur = Array[I];
281       u32 J = I;
282       while (J > 0 && compactPtrGroup(Cur) < compactPtrGroup(Array[J - 1])) {
283         Array[J] = Array[J - 1];
284         --J;
285       }
286       Array[J] = Cur;
287     }
288 
289     {
290       ScopedLock L(Region->FLLock);
291       pushBlocksImpl(C, ClassId, Region, Array, Size, SameGroup);
292     }
293   }
294 
295   void disable() NO_THREAD_SAFETY_ANALYSIS {
296     // The BatchClassId must be locked last since other classes can use it.
297     for (sptr I = static_cast<sptr>(NumClasses) - 1; I >= 0; I--) {
298       if (static_cast<uptr>(I) == SizeClassMap::BatchClassId)
299         continue;
300       getRegionInfo(static_cast<uptr>(I))->MMLock.lock();
301       getRegionInfo(static_cast<uptr>(I))->FLLock.lock();
302     }
303     getRegionInfo(SizeClassMap::BatchClassId)->MMLock.lock();
304     getRegionInfo(SizeClassMap::BatchClassId)->FLLock.lock();
305   }
306 
307   void enable() NO_THREAD_SAFETY_ANALYSIS {
308     getRegionInfo(SizeClassMap::BatchClassId)->FLLock.unlock();
309     getRegionInfo(SizeClassMap::BatchClassId)->MMLock.unlock();
310     for (uptr I = 0; I < NumClasses; I++) {
311       if (I == SizeClassMap::BatchClassId)
312         continue;
313       getRegionInfo(I)->FLLock.unlock();
314       getRegionInfo(I)->MMLock.unlock();
315     }
316   }
317 
318   template <typename F> void iterateOverBlocks(F Callback) {
319     for (uptr I = 0; I < NumClasses; I++) {
320       if (I == SizeClassMap::BatchClassId)
321         continue;
322       RegionInfo *Region = getRegionInfo(I);
323       // TODO: The call of `iterateOverBlocks` requires disabling
324       // SizeClassAllocator64. We may consider locking each region on demand
325       // only.
326       Region->FLLock.assertHeld();
327       Region->MMLock.assertHeld();
328       const uptr BlockSize = getSizeByClassId(I);
329       const uptr From = Region->RegionBeg;
330       const uptr To = From + Region->MemMapInfo.AllocatedUser;
331       for (uptr Block = From; Block < To; Block += BlockSize)
332         Callback(Block);
333     }
334   }
335 
336   void getStats(ScopedString *Str) {
337     // TODO(kostyak): get the RSS per region.
338     uptr TotalMapped = 0;
339     uptr PoppedBlocks = 0;
340     uptr PushedBlocks = 0;
341     for (uptr I = 0; I < NumClasses; I++) {
342       RegionInfo *Region = getRegionInfo(I);
343       {
344         ScopedLock L(Region->MMLock);
345         TotalMapped += Region->MemMapInfo.MappedUser;
346       }
347       {
348         ScopedLock L(Region->FLLock);
349         PoppedBlocks += Region->FreeListInfo.PoppedBlocks;
350         PushedBlocks += Region->FreeListInfo.PushedBlocks;
351       }
352     }
353     Str->append("Stats: SizeClassAllocator64: %zuM mapped (%uM rss) in %zu "
354                 "allocations; remains %zu\n",
355                 TotalMapped >> 20, 0U, PoppedBlocks,
356                 PoppedBlocks - PushedBlocks);
357 
358     for (uptr I = 0; I < NumClasses; I++) {
359       RegionInfo *Region = getRegionInfo(I);
360       ScopedLock L1(Region->MMLock);
361       ScopedLock L2(Region->FLLock);
362       getStats(Str, I, Region);
363     }
364   }
365 
366   bool setOption(Option O, sptr Value) {
367     if (O == Option::ReleaseInterval) {
368       const s32 Interval = Max(Min(static_cast<s32>(Value),
369                                    Config::Primary::MaxReleaseToOsIntervalMs),
370                                Config::Primary::MinReleaseToOsIntervalMs);
371       atomic_store_relaxed(&ReleaseToOsIntervalMs, Interval);
372       return true;
373     }
374     // Not supported by the Primary, but not an error either.
375     return true;
376   }
377 
378   uptr tryReleaseToOS(uptr ClassId, ReleaseToOS ReleaseType) {
379     RegionInfo *Region = getRegionInfo(ClassId);
380     // Note that the tryLock() may fail spuriously, given that it should rarely
381     // happen and page releasing is fine to skip, we don't take certain
382     // approaches to ensure one page release is done.
383     if (Region->MMLock.tryLock()) {
384       uptr BytesReleased = releaseToOSMaybe(Region, ClassId, ReleaseType);
385       Region->MMLock.unlock();
386       return BytesReleased;
387     }
388     return 0;
389   }
390 
391   uptr releaseToOS(ReleaseToOS ReleaseType) {
392     uptr TotalReleasedBytes = 0;
393     for (uptr I = 0; I < NumClasses; I++) {
394       if (I == SizeClassMap::BatchClassId)
395         continue;
396       RegionInfo *Region = getRegionInfo(I);
397       ScopedLock L(Region->MMLock);
398       TotalReleasedBytes += releaseToOSMaybe(Region, I, ReleaseType);
399     }
400     return TotalReleasedBytes;
401   }
402 
403   const char *getRegionInfoArrayAddress() const {
404     return reinterpret_cast<const char *>(RegionInfoArray);
405   }
406 
407   static uptr getRegionInfoArraySize() { return sizeof(RegionInfoArray); }
408 
409   uptr getCompactPtrBaseByClassId(uptr ClassId) {
410     return getRegionInfo(ClassId)->RegionBeg;
411   }
412 
413   CompactPtrT compactPtr(uptr ClassId, uptr Ptr) {
414     DCHECK_LE(ClassId, SizeClassMap::LargestClassId);
415     return compactPtrInternal(getCompactPtrBaseByClassId(ClassId), Ptr);
416   }
417 
418   void *decompactPtr(uptr ClassId, CompactPtrT CompactPtr) {
419     DCHECK_LE(ClassId, SizeClassMap::LargestClassId);
420     return reinterpret_cast<void *>(
421         decompactPtrInternal(getCompactPtrBaseByClassId(ClassId), CompactPtr));
422   }
423 
424   static BlockInfo findNearestBlock(const char *RegionInfoData,
425                                     uptr Ptr) NO_THREAD_SAFETY_ANALYSIS {
426     const RegionInfo *RegionInfoArray =
427         reinterpret_cast<const RegionInfo *>(RegionInfoData);
428 
429     uptr ClassId;
430     uptr MinDistance = -1UL;
431     for (uptr I = 0; I != NumClasses; ++I) {
432       if (I == SizeClassMap::BatchClassId)
433         continue;
434       uptr Begin = RegionInfoArray[I].RegionBeg;
435       // TODO(chiahungduan): In fact, We need to lock the RegionInfo::MMLock.
436       // However, the RegionInfoData is passed with const qualifier and lock the
437       // mutex requires modifying RegionInfoData, which means we need to remove
438       // the const qualifier. This may lead to another undefined behavior (The
439       // first one is accessing `AllocatedUser` without locking. It's better to
440       // pass `RegionInfoData` as `void *` then we can lock the mutex properly.
441       uptr End = Begin + RegionInfoArray[I].MemMapInfo.AllocatedUser;
442       if (Begin > End || End - Begin < SizeClassMap::getSizeByClassId(I))
443         continue;
444       uptr RegionDistance;
445       if (Begin <= Ptr) {
446         if (Ptr < End)
447           RegionDistance = 0;
448         else
449           RegionDistance = Ptr - End;
450       } else {
451         RegionDistance = Begin - Ptr;
452       }
453 
454       if (RegionDistance < MinDistance) {
455         MinDistance = RegionDistance;
456         ClassId = I;
457       }
458     }
459 
460     BlockInfo B = {};
461     if (MinDistance <= 8192) {
462       B.RegionBegin = RegionInfoArray[ClassId].RegionBeg;
463       B.RegionEnd =
464           B.RegionBegin + RegionInfoArray[ClassId].MemMapInfo.AllocatedUser;
465       B.BlockSize = SizeClassMap::getSizeByClassId(ClassId);
466       B.BlockBegin =
467           B.RegionBegin + uptr(sptr(Ptr - B.RegionBegin) / sptr(B.BlockSize) *
468                                sptr(B.BlockSize));
469       while (B.BlockBegin < B.RegionBegin)
470         B.BlockBegin += B.BlockSize;
471       while (B.RegionEnd < B.BlockBegin + B.BlockSize)
472         B.BlockBegin -= B.BlockSize;
473     }
474     return B;
475   }
476 
477   AtomicOptions Options;
478 
479 private:
480   static const uptr RegionSize = 1UL << Config::Primary::RegionSizeLog;
481   static const uptr NumClasses = SizeClassMap::NumClasses;
482   static const uptr PrimarySize = RegionSize * NumClasses;
483 
484   static const uptr MapSizeIncrement = Config::Primary::MapSizeIncrement;
485   // Fill at most this number of batches from the newly map'd memory.
486   static const u32 MaxNumBatches = SCUDO_ANDROID ? 4U : 8U;
487 
488   struct ReleaseToOsInfo {
489     uptr BytesInFreeListAtLastCheckpoint;
490     uptr RangesReleased;
491     uptr LastReleasedBytes;
492     u64 LastReleaseAtNs;
493   };
494 
495   struct BlocksInfo {
496     SinglyLinkedList<BatchGroup> BlockList = {};
497     uptr PoppedBlocks = 0;
498     uptr PushedBlocks = 0;
499   };
500 
501   struct PagesInfo {
502     MemMapT MemMap = {};
503     // Bytes mapped for user memory.
504     uptr MappedUser = 0;
505     // Bytes allocated for user memory.
506     uptr AllocatedUser = 0;
507   };
508 
509   struct UnpaddedRegionInfo {
510     // Mutex for operations on freelist
511     HybridMutex FLLock;
512     // Mutex for memmap operations
513     HybridMutex MMLock ACQUIRED_BEFORE(FLLock);
514     // `RegionBeg` is initialized before thread creation and won't be changed.
515     uptr RegionBeg = 0;
516     u32 RandState GUARDED_BY(MMLock) = 0;
517     BlocksInfo FreeListInfo GUARDED_BY(FLLock);
518     PagesInfo MemMapInfo GUARDED_BY(MMLock);
519     // The minimum size of pushed blocks to trigger page release.
520     uptr TryReleaseThreshold GUARDED_BY(MMLock) = 0;
521     ReleaseToOsInfo ReleaseInfo GUARDED_BY(MMLock) = {};
522     bool Exhausted GUARDED_BY(MMLock) = false;
523   };
524   struct RegionInfo : UnpaddedRegionInfo {
525     char Padding[SCUDO_CACHE_LINE_SIZE -
526                  (sizeof(UnpaddedRegionInfo) % SCUDO_CACHE_LINE_SIZE)] = {};
527   };
528   static_assert(sizeof(RegionInfo) % SCUDO_CACHE_LINE_SIZE == 0, "");
529 
530   RegionInfo *getRegionInfo(uptr ClassId) {
531     DCHECK_LT(ClassId, NumClasses);
532     return &RegionInfoArray[ClassId];
533   }
534 
535   uptr getRegionBaseByClassId(uptr ClassId) {
536     return roundDown(getRegionInfo(ClassId)->RegionBeg - PrimaryBase,
537                      RegionSize) +
538            PrimaryBase;
539   }
540 
541   static CompactPtrT compactPtrInternal(uptr Base, uptr Ptr) {
542     return static_cast<CompactPtrT>((Ptr - Base) >> CompactPtrScale);
543   }
544 
545   static uptr decompactPtrInternal(uptr Base, CompactPtrT CompactPtr) {
546     return Base + (static_cast<uptr>(CompactPtr) << CompactPtrScale);
547   }
548 
549   static uptr compactPtrGroup(CompactPtrT CompactPtr) {
550     const uptr Mask = (static_cast<uptr>(1) << GroupScale) - 1;
551     return static_cast<uptr>(CompactPtr) & ~Mask;
552   }
553   static uptr decompactGroupBase(uptr Base, uptr CompactPtrGroupBase) {
554     DCHECK_EQ(CompactPtrGroupBase % (static_cast<uptr>(1) << (GroupScale)), 0U);
555     return Base + (CompactPtrGroupBase << CompactPtrScale);
556   }
557 
558   ALWAYS_INLINE static bool isSmallBlock(uptr BlockSize) {
559     const uptr PageSize = getPageSizeCached();
560     return BlockSize < PageSize / 16U;
561   }
562 
563   ALWAYS_INLINE static bool isLargeBlock(uptr BlockSize) {
564     const uptr PageSize = getPageSizeCached();
565     return BlockSize > PageSize;
566   }
567 
568   void pushBatchClassBlocks(RegionInfo *Region, CompactPtrT *Array, u32 Size)
569       REQUIRES(Region->FLLock) {
570     DCHECK_EQ(Region, getRegionInfo(SizeClassMap::BatchClassId));
571 
572     // Free blocks are recorded by TransferBatch in freelist for all
573     // size-classes. In addition, TransferBatch is allocated from BatchClassId.
574     // In order not to use additional block to record the free blocks in
575     // BatchClassId, they are self-contained. I.e., A TransferBatch records the
576     // block address of itself. See the figure below:
577     //
578     // TransferBatch at 0xABCD
579     // +----------------------------+
580     // | Free blocks' addr          |
581     // | +------+------+------+     |
582     // | |0xABCD|...   |...   |     |
583     // | +------+------+------+     |
584     // +----------------------------+
585     //
586     // When we allocate all the free blocks in the TransferBatch, the block used
587     // by TransferBatch is also free for use. We don't need to recycle the
588     // TransferBatch. Note that the correctness is maintained by the invariant,
589     //
590     //   The unit of each popBatch() request is entire TransferBatch. Return
591     //   part of the blocks in a TransferBatch is invalid.
592     //
593     // This ensures that TransferBatch won't leak the address itself while it's
594     // still holding other valid data.
595     //
596     // Besides, BatchGroup is also allocated from BatchClassId and has its
597     // address recorded in the TransferBatch too. To maintain the correctness,
598     //
599     //   The address of BatchGroup is always recorded in the last TransferBatch
600     //   in the freelist (also imply that the freelist should only be
601     //   updated with push_front). Once the last TransferBatch is popped,
602     //   the block used by BatchGroup is also free for use.
603     //
604     // With this approach, the blocks used by BatchGroup and TransferBatch are
605     // reusable and don't need additional space for them.
606 
607     Region->FreeListInfo.PushedBlocks += Size;
608     BatchGroup *BG = Region->FreeListInfo.BlockList.front();
609 
610     if (BG == nullptr) {
611       // Construct `BatchGroup` on the last element.
612       BG = reinterpret_cast<BatchGroup *>(
613           decompactPtr(SizeClassMap::BatchClassId, Array[Size - 1]));
614       --Size;
615       BG->Batches.clear();
616       // BatchClass hasn't enabled memory group. Use `0` to indicate there's no
617       // memory group here.
618       BG->CompactPtrGroupBase = 0;
619       // `BG` is also the block of BatchClassId. Note that this is different
620       // from `CreateGroup` in `pushBlocksImpl`
621       BG->PushedBlocks = 1;
622       BG->BytesInBGAtLastCheckpoint = 0;
623       BG->MaxCachedPerBatch = TransferBatch::getMaxCached(
624           getSizeByClassId(SizeClassMap::BatchClassId));
625 
626       Region->FreeListInfo.BlockList.push_front(BG);
627     }
628 
629     if (UNLIKELY(Size == 0))
630       return;
631 
632     // This happens under 2 cases.
633     //   1. just allocated a new `BatchGroup`.
634     //   2. Only 1 block is pushed when the freelist is empty.
635     if (BG->Batches.empty()) {
636       // Construct the `TransferBatch` on the last element.
637       TransferBatch *TB = reinterpret_cast<TransferBatch *>(
638           decompactPtr(SizeClassMap::BatchClassId, Array[Size - 1]));
639       TB->clear();
640       // As mentioned above, addresses of `TransferBatch` and `BatchGroup` are
641       // recorded in the TransferBatch.
642       TB->add(Array[Size - 1]);
643       TB->add(
644           compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(BG)));
645       --Size;
646       DCHECK_EQ(BG->PushedBlocks, 1U);
647       // `TB` is also the block of BatchClassId.
648       BG->PushedBlocks += 1;
649       BG->Batches.push_front(TB);
650     }
651 
652     TransferBatch *CurBatch = BG->Batches.front();
653     DCHECK_NE(CurBatch, nullptr);
654 
655     for (u32 I = 0; I < Size;) {
656       u16 UnusedSlots =
657           static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount());
658       if (UnusedSlots == 0) {
659         CurBatch = reinterpret_cast<TransferBatch *>(
660             decompactPtr(SizeClassMap::BatchClassId, Array[I]));
661         CurBatch->clear();
662         // Self-contained
663         CurBatch->add(Array[I]);
664         ++I;
665         // TODO(chiahungduan): Avoid the use of push_back() in `Batches` of
666         // BatchClassId.
667         BG->Batches.push_front(CurBatch);
668         UnusedSlots = static_cast<u16>(BG->MaxCachedPerBatch - 1);
669       }
670       // `UnusedSlots` is u16 so the result will be also fit in u16.
671       const u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I));
672       CurBatch->appendFromArray(&Array[I], AppendSize);
673       I += AppendSize;
674     }
675 
676     BG->PushedBlocks += Size;
677   }
678 
679   // Push the blocks to their batch group. The layout will be like,
680   //
681   // FreeListInfo.BlockList - > BG -> BG -> BG
682   //                            |     |     |
683   //                            v     v     v
684   //                            TB    TB    TB
685   //                            |
686   //                            v
687   //                            TB
688   //
689   // Each BlockGroup(BG) will associate with unique group id and the free blocks
690   // are managed by a list of TransferBatch(TB). To reduce the time of inserting
691   // blocks, BGs are sorted and the input `Array` are supposed to be sorted so
692   // that we can get better performance of maintaining sorted property.
693   // Use `SameGroup=true` to indicate that all blocks in the array are from the
694   // same group then we will skip checking the group id of each block.
695   void pushBlocksImpl(CacheT *C, uptr ClassId, RegionInfo *Region,
696                       CompactPtrT *Array, u32 Size, bool SameGroup = false)
697       REQUIRES(Region->FLLock) {
698     DCHECK_NE(ClassId, SizeClassMap::BatchClassId);
699     DCHECK_GT(Size, 0U);
700 
701     auto CreateGroup = [&](uptr CompactPtrGroupBase) {
702       BatchGroup *BG = C->createGroup();
703       BG->Batches.clear();
704       TransferBatch *TB = C->createBatch(ClassId, nullptr);
705       TB->clear();
706 
707       BG->CompactPtrGroupBase = CompactPtrGroupBase;
708       BG->Batches.push_front(TB);
709       BG->PushedBlocks = 0;
710       BG->BytesInBGAtLastCheckpoint = 0;
711       BG->MaxCachedPerBatch =
712           TransferBatch::getMaxCached(getSizeByClassId(ClassId));
713 
714       return BG;
715     };
716 
717     auto InsertBlocks = [&](BatchGroup *BG, CompactPtrT *Array, u32 Size) {
718       SinglyLinkedList<TransferBatch> &Batches = BG->Batches;
719       TransferBatch *CurBatch = Batches.front();
720       DCHECK_NE(CurBatch, nullptr);
721 
722       for (u32 I = 0; I < Size;) {
723         DCHECK_GE(BG->MaxCachedPerBatch, CurBatch->getCount());
724         u16 UnusedSlots =
725             static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount());
726         if (UnusedSlots == 0) {
727           CurBatch = C->createBatch(
728               ClassId,
729               reinterpret_cast<void *>(decompactPtr(ClassId, Array[I])));
730           CurBatch->clear();
731           Batches.push_front(CurBatch);
732           UnusedSlots = BG->MaxCachedPerBatch;
733         }
734         // `UnusedSlots` is u16 so the result will be also fit in u16.
735         u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I));
736         CurBatch->appendFromArray(&Array[I], AppendSize);
737         I += AppendSize;
738       }
739 
740       BG->PushedBlocks += Size;
741     };
742 
743     Region->FreeListInfo.PushedBlocks += Size;
744     BatchGroup *Cur = Region->FreeListInfo.BlockList.front();
745 
746     if (ClassId == SizeClassMap::BatchClassId) {
747       if (Cur == nullptr) {
748         // Don't need to classify BatchClassId.
749         Cur = CreateGroup(/*CompactPtrGroupBase=*/0);
750         Region->FreeListInfo.BlockList.push_front(Cur);
751       }
752       InsertBlocks(Cur, Array, Size);
753       return;
754     }
755 
756     // In the following, `Cur` always points to the BatchGroup for blocks that
757     // will be pushed next. `Prev` is the element right before `Cur`.
758     BatchGroup *Prev = nullptr;
759 
760     while (Cur != nullptr &&
761            compactPtrGroup(Array[0]) > Cur->CompactPtrGroupBase) {
762       Prev = Cur;
763       Cur = Cur->Next;
764     }
765 
766     if (Cur == nullptr ||
767         compactPtrGroup(Array[0]) != Cur->CompactPtrGroupBase) {
768       Cur = CreateGroup(compactPtrGroup(Array[0]));
769       if (Prev == nullptr)
770         Region->FreeListInfo.BlockList.push_front(Cur);
771       else
772         Region->FreeListInfo.BlockList.insert(Prev, Cur);
773     }
774 
775     // All the blocks are from the same group, just push without checking group
776     // id.
777     if (SameGroup) {
778       for (u32 I = 0; I < Size; ++I)
779         DCHECK_EQ(compactPtrGroup(Array[I]), Cur->CompactPtrGroupBase);
780 
781       InsertBlocks(Cur, Array, Size);
782       return;
783     }
784 
785     // The blocks are sorted by group id. Determine the segment of group and
786     // push them to their group together.
787     u32 Count = 1;
788     for (u32 I = 1; I < Size; ++I) {
789       if (compactPtrGroup(Array[I - 1]) != compactPtrGroup(Array[I])) {
790         DCHECK_EQ(compactPtrGroup(Array[I - 1]), Cur->CompactPtrGroupBase);
791         InsertBlocks(Cur, Array + I - Count, Count);
792 
793         while (Cur != nullptr &&
794                compactPtrGroup(Array[I]) > Cur->CompactPtrGroupBase) {
795           Prev = Cur;
796           Cur = Cur->Next;
797         }
798 
799         if (Cur == nullptr ||
800             compactPtrGroup(Array[I]) != Cur->CompactPtrGroupBase) {
801           Cur = CreateGroup(compactPtrGroup(Array[I]));
802           DCHECK_NE(Prev, nullptr);
803           Region->FreeListInfo.BlockList.insert(Prev, Cur);
804         }
805 
806         Count = 1;
807       } else {
808         ++Count;
809       }
810     }
811 
812     InsertBlocks(Cur, Array + Size - Count, Count);
813   }
814 
815   // Pop one TransferBatch from a BatchGroup. The BatchGroup with the smallest
816   // group id will be considered first.
817   //
818   // The region mutex needs to be held while calling this method.
819   TransferBatch *popBatchImpl(CacheT *C, uptr ClassId, RegionInfo *Region)
820       REQUIRES(Region->FLLock) {
821     if (Region->FreeListInfo.BlockList.empty())
822       return nullptr;
823 
824     SinglyLinkedList<TransferBatch> &Batches =
825         Region->FreeListInfo.BlockList.front()->Batches;
826 
827     if (Batches.empty()) {
828       DCHECK_EQ(ClassId, SizeClassMap::BatchClassId);
829       BatchGroup *BG = Region->FreeListInfo.BlockList.front();
830       Region->FreeListInfo.BlockList.pop_front();
831 
832       // Block used by `BatchGroup` is from BatchClassId. Turn the block into
833       // `TransferBatch` with single block.
834       TransferBatch *TB = reinterpret_cast<TransferBatch *>(BG);
835       TB->clear();
836       TB->add(
837           compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(TB)));
838       Region->FreeListInfo.PoppedBlocks += 1;
839       return TB;
840     }
841 
842     TransferBatch *B = Batches.front();
843     Batches.pop_front();
844     DCHECK_NE(B, nullptr);
845     DCHECK_GT(B->getCount(), 0U);
846 
847     if (Batches.empty()) {
848       BatchGroup *BG = Region->FreeListInfo.BlockList.front();
849       Region->FreeListInfo.BlockList.pop_front();
850 
851       // We don't keep BatchGroup with zero blocks to avoid empty-checking while
852       // allocating. Note that block used by constructing BatchGroup is recorded
853       // as free blocks in the last element of BatchGroup::Batches. Which means,
854       // once we pop the last TransferBatch, the block is implicitly
855       // deallocated.
856       if (ClassId != SizeClassMap::BatchClassId)
857         C->deallocate(SizeClassMap::BatchClassId, BG);
858     }
859 
860     Region->FreeListInfo.PoppedBlocks += B->getCount();
861 
862     return B;
863   }
864 
865   // Refill the freelist and return one batch.
866   NOINLINE TransferBatch *populateFreeListAndPopBatch(CacheT *C, uptr ClassId,
867                                                       RegionInfo *Region)
868       REQUIRES(Region->MMLock) EXCLUDES(Region->FLLock) {
869     const uptr Size = getSizeByClassId(ClassId);
870     const u16 MaxCount = TransferBatch::getMaxCached(Size);
871 
872     const uptr RegionBeg = Region->RegionBeg;
873     const uptr MappedUser = Region->MemMapInfo.MappedUser;
874     const uptr TotalUserBytes =
875         Region->MemMapInfo.AllocatedUser + MaxCount * Size;
876     // Map more space for blocks, if necessary.
877     if (TotalUserBytes > MappedUser) {
878       // Do the mmap for the user memory.
879       const uptr MapSize =
880           roundUp(TotalUserBytes - MappedUser, MapSizeIncrement);
881       const uptr RegionBase = RegionBeg - getRegionBaseByClassId(ClassId);
882       if (UNLIKELY(RegionBase + MappedUser + MapSize > RegionSize)) {
883         Region->Exhausted = true;
884         return nullptr;
885       }
886 
887       if (UNLIKELY(!Region->MemMapInfo.MemMap.remap(
888               RegionBeg + MappedUser, MapSize, "scudo:primary",
889               MAP_ALLOWNOMEM | MAP_RESIZABLE |
890                   (useMemoryTagging<Config>(Options.load()) ? MAP_MEMTAG
891                                                             : 0)))) {
892         return nullptr;
893       }
894       Region->MemMapInfo.MappedUser += MapSize;
895       C->getStats().add(StatMapped, MapSize);
896     }
897 
898     const u32 NumberOfBlocks =
899         Min(MaxNumBatches * MaxCount,
900             static_cast<u32>((Region->MemMapInfo.MappedUser -
901                               Region->MemMapInfo.AllocatedUser) /
902                              Size));
903     DCHECK_GT(NumberOfBlocks, 0);
904 
905     constexpr u32 ShuffleArraySize =
906         MaxNumBatches * TransferBatch::MaxNumCached;
907     CompactPtrT ShuffleArray[ShuffleArraySize];
908     DCHECK_LE(NumberOfBlocks, ShuffleArraySize);
909 
910     const uptr CompactPtrBase = getCompactPtrBaseByClassId(ClassId);
911     uptr P = RegionBeg + Region->MemMapInfo.AllocatedUser;
912     for (u32 I = 0; I < NumberOfBlocks; I++, P += Size)
913       ShuffleArray[I] = compactPtrInternal(CompactPtrBase, P);
914 
915     ScopedLock L(Region->FLLock);
916 
917     if (ClassId != SizeClassMap::BatchClassId) {
918       u32 N = 1;
919       uptr CurGroup = compactPtrGroup(ShuffleArray[0]);
920       for (u32 I = 1; I < NumberOfBlocks; I++) {
921         if (UNLIKELY(compactPtrGroup(ShuffleArray[I]) != CurGroup)) {
922           shuffle(ShuffleArray + I - N, N, &Region->RandState);
923           pushBlocksImpl(C, ClassId, Region, ShuffleArray + I - N, N,
924                          /*SameGroup=*/true);
925           N = 1;
926           CurGroup = compactPtrGroup(ShuffleArray[I]);
927         } else {
928           ++N;
929         }
930       }
931 
932       shuffle(ShuffleArray + NumberOfBlocks - N, N, &Region->RandState);
933       pushBlocksImpl(C, ClassId, Region, &ShuffleArray[NumberOfBlocks - N], N,
934                      /*SameGroup=*/true);
935     } else {
936       pushBatchClassBlocks(Region, ShuffleArray, NumberOfBlocks);
937     }
938 
939     TransferBatch *B = popBatchImpl(C, ClassId, Region);
940     DCHECK_NE(B, nullptr);
941 
942     // Note that `PushedBlocks` and `PoppedBlocks` are supposed to only record
943     // the requests from `PushBlocks` and `PopBatch` which are external
944     // interfaces. `populateFreeListAndPopBatch` is the internal interface so we
945     // should set the values back to avoid incorrectly setting the stats.
946     Region->FreeListInfo.PushedBlocks -= NumberOfBlocks;
947 
948     const uptr AllocatedUser = Size * NumberOfBlocks;
949     C->getStats().add(StatFree, AllocatedUser);
950     Region->MemMapInfo.AllocatedUser += AllocatedUser;
951 
952     return B;
953   }
954 
955   void getStats(ScopedString *Str, uptr ClassId, RegionInfo *Region)
956       REQUIRES(Region->MMLock, Region->FLLock) {
957     if (Region->MemMapInfo.MappedUser == 0)
958       return;
959     const uptr BlockSize = getSizeByClassId(ClassId);
960     const uptr InUse =
961         Region->FreeListInfo.PoppedBlocks - Region->FreeListInfo.PushedBlocks;
962     const uptr BytesInFreeList =
963         Region->MemMapInfo.AllocatedUser - InUse * BlockSize;
964     uptr RegionPushedBytesDelta = 0;
965     if (BytesInFreeList >=
966         Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint) {
967       RegionPushedBytesDelta =
968           BytesInFreeList - Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint;
969     }
970     const uptr TotalChunks = Region->MemMapInfo.AllocatedUser / BlockSize;
971     Str->append(
972         "%s %02zu (%6zu): mapped: %6zuK popped: %7zu pushed: %7zu "
973         "inuse: %6zu total: %6zu releases: %6zu last "
974         "released: %6zuK latest pushed bytes: %6zuK region: 0x%zx (0x%zx)\n",
975         Region->Exhausted ? "F" : " ", ClassId, getSizeByClassId(ClassId),
976         Region->MemMapInfo.MappedUser >> 10, Region->FreeListInfo.PoppedBlocks,
977         Region->FreeListInfo.PushedBlocks, InUse, TotalChunks,
978         Region->ReleaseInfo.RangesReleased,
979         Region->ReleaseInfo.LastReleasedBytes >> 10,
980         RegionPushedBytesDelta >> 10, Region->RegionBeg,
981         getRegionBaseByClassId(ClassId));
982   }
983 
984   NOINLINE uptr releaseToOSMaybe(RegionInfo *Region, uptr ClassId,
985                                  ReleaseToOS ReleaseType = ReleaseToOS::Normal)
986       REQUIRES(Region->MMLock) EXCLUDES(Region->FLLock) {
987     ScopedLock L(Region->FLLock);
988 
989     const uptr BlockSize = getSizeByClassId(ClassId);
990     const uptr BytesInFreeList =
991         Region->MemMapInfo.AllocatedUser - (Region->FreeListInfo.PoppedBlocks -
992                                             Region->FreeListInfo.PushedBlocks) *
993                                                BlockSize;
994     if (UNLIKELY(BytesInFreeList == 0))
995       return false;
996 
997     const uptr AllocatedUserEnd =
998         Region->MemMapInfo.AllocatedUser + Region->RegionBeg;
999     const uptr CompactPtrBase = getCompactPtrBaseByClassId(ClassId);
1000 
1001     // ====================================================================== //
1002     // 1. Check if we have enough free blocks and if it's worth doing a page
1003     //    release.
1004     // ====================================================================== //
1005     if (ReleaseType != ReleaseToOS::ForceAll &&
1006         !hasChanceToReleasePages(Region, BlockSize, BytesInFreeList,
1007                                  ReleaseType)) {
1008       return 0;
1009     }
1010 
1011     // ====================================================================== //
1012     // 2. Determine which groups can release the pages. Use a heuristic to
1013     //    gather groups that are candidates for doing a release.
1014     // ====================================================================== //
1015     SinglyLinkedList<BatchGroup> GroupsToRelease;
1016     if (ReleaseType == ReleaseToOS::ForceAll) {
1017       GroupsToRelease = Region->FreeListInfo.BlockList;
1018       Region->FreeListInfo.BlockList.clear();
1019     } else {
1020       GroupsToRelease = collectGroupsToRelease(
1021           Region, BlockSize, AllocatedUserEnd, CompactPtrBase);
1022     }
1023     if (GroupsToRelease.empty())
1024       return 0;
1025 
1026     // Ideally, we should use a class like `ScopedUnlock`. However, this form of
1027     // unlocking is not supported by the thread-safety analysis. See
1028     // https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#no-alias-analysis
1029     // for more details.
1030     // Put it as local class so that we can mark the ctor/dtor with proper
1031     // annotations associated to the target lock. Note that accessing the
1032     // function variable in local class only works in thread-safety annotations.
1033     // TODO: Implement general `ScopedUnlock` when it's supported.
1034     class FLLockScopedUnlock {
1035     public:
1036       FLLockScopedUnlock(RegionInfo *Region) RELEASE(Region->FLLock)
1037           : R(Region) {
1038         R->FLLock.assertHeld();
1039         R->FLLock.unlock();
1040       }
1041       ~FLLockScopedUnlock() ACQUIRE(Region->FLLock) { R->FLLock.lock(); }
1042 
1043     private:
1044       RegionInfo *R;
1045     };
1046 
1047     // Note that we have extracted the `GroupsToRelease` from region freelist.
1048     // It's safe to let pushBlocks()/popBatches() access the remaining region
1049     // freelist. In the steps 3 and 4, we will temporarily release the FLLock
1050     // and lock it again before step 5.
1051 
1052     uptr ReleasedBytes = 0;
1053     {
1054       FLLockScopedUnlock UL(Region);
1055       // ==================================================================== //
1056       // 3. Mark the free blocks in `GroupsToRelease` in the
1057       //    `PageReleaseContext`. Then we can tell which pages are in-use by
1058       //    querying `PageReleaseContext`.
1059       // ==================================================================== //
1060       PageReleaseContext Context = markFreeBlocks(
1061           Region, BlockSize, AllocatedUserEnd, CompactPtrBase, GroupsToRelease);
1062       if (UNLIKELY(!Context.hasBlockMarked())) {
1063         ScopedLock L(Region->FLLock);
1064         mergeGroupsToReleaseBack(Region, GroupsToRelease);
1065         return 0;
1066       }
1067 
1068       // ==================================================================== //
1069       // 4. Release the unused physical pages back to the OS.
1070       // ==================================================================== //
1071       RegionReleaseRecorder<MemMapT> Recorder(&Region->MemMapInfo.MemMap,
1072                                               Region->RegionBeg,
1073                                               Context.getReleaseOffset());
1074       auto SkipRegion = [](UNUSED uptr RegionIndex) { return false; };
1075       releaseFreeMemoryToOS(Context, Recorder, SkipRegion);
1076       if (Recorder.getReleasedRangesCount() > 0) {
1077         Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;
1078         Region->ReleaseInfo.RangesReleased += Recorder.getReleasedRangesCount();
1079         Region->ReleaseInfo.LastReleasedBytes = Recorder.getReleasedBytes();
1080       }
1081       Region->ReleaseInfo.LastReleaseAtNs = getMonotonicTimeFast();
1082       ReleasedBytes = Recorder.getReleasedBytes();
1083     }
1084 
1085     // ====================================================================== //
1086     // 5. Merge the `GroupsToRelease` back to the freelist.
1087     // ====================================================================== //
1088     mergeGroupsToReleaseBack(Region, GroupsToRelease);
1089 
1090     return ReleasedBytes;
1091   }
1092 
1093   bool hasChanceToReleasePages(RegionInfo *Region, uptr BlockSize,
1094                                uptr BytesInFreeList, ReleaseToOS ReleaseType)
1095       REQUIRES(Region->MMLock, Region->FLLock) {
1096     DCHECK_GE(Region->FreeListInfo.PoppedBlocks,
1097               Region->FreeListInfo.PushedBlocks);
1098     const uptr PageSize = getPageSizeCached();
1099 
1100     // Always update `BytesInFreeListAtLastCheckpoint` with the smallest value
1101     // so that we won't underestimate the releasable pages. For example, the
1102     // following is the region usage,
1103     //
1104     //  BytesInFreeListAtLastCheckpoint   AllocatedUser
1105     //                v                         v
1106     //  |--------------------------------------->
1107     //         ^                   ^
1108     //  BytesInFreeList     ReleaseThreshold
1109     //
1110     // In general, if we have collected enough bytes and the amount of free
1111     // bytes meets the ReleaseThreshold, we will try to do page release. If we
1112     // don't update `BytesInFreeListAtLastCheckpoint` when the current
1113     // `BytesInFreeList` is smaller, we may take longer time to wait for enough
1114     // freed blocks because we miss the bytes between
1115     // (BytesInFreeListAtLastCheckpoint - BytesInFreeList).
1116     if (BytesInFreeList <=
1117         Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint) {
1118       Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;
1119     }
1120 
1121     const uptr RegionPushedBytesDelta =
1122         BytesInFreeList - Region->ReleaseInfo.BytesInFreeListAtLastCheckpoint;
1123     if (RegionPushedBytesDelta < PageSize)
1124       return false;
1125 
1126     // Releasing smaller blocks is expensive, so we want to make sure that a
1127     // significant amount of bytes are free, and that there has been a good
1128     // amount of batches pushed to the freelist before attempting to release.
1129     if (isSmallBlock(BlockSize) && ReleaseType == ReleaseToOS::Normal)
1130       if (RegionPushedBytesDelta < Region->TryReleaseThreshold)
1131         return false;
1132 
1133     if (ReleaseType == ReleaseToOS::Normal) {
1134       const s32 IntervalMs = atomic_load_relaxed(&ReleaseToOsIntervalMs);
1135       if (IntervalMs < 0)
1136         return false;
1137 
1138       // The constant 8 here is selected from profiling some apps and the number
1139       // of unreleased pages in the large size classes is around 16 pages or
1140       // more. Choose half of it as a heuristic and which also avoids page
1141       // release every time for every pushBlocks() attempt by large blocks.
1142       const bool ByPassReleaseInterval =
1143           isLargeBlock(BlockSize) && RegionPushedBytesDelta > 8 * PageSize;
1144       if (!ByPassReleaseInterval) {
1145         if (Region->ReleaseInfo.LastReleaseAtNs +
1146                 static_cast<u64>(IntervalMs) * 1000000 >
1147             getMonotonicTimeFast()) {
1148           // Memory was returned recently.
1149           return false;
1150         }
1151       }
1152     } // if (ReleaseType == ReleaseToOS::Normal)
1153 
1154     return true;
1155   }
1156 
1157   SinglyLinkedList<BatchGroup>
1158   collectGroupsToRelease(RegionInfo *Region, const uptr BlockSize,
1159                          const uptr AllocatedUserEnd, const uptr CompactPtrBase)
1160       REQUIRES(Region->MMLock, Region->FLLock) {
1161     const uptr GroupSize = (1U << GroupSizeLog);
1162     const uptr PageSize = getPageSizeCached();
1163     SinglyLinkedList<BatchGroup> GroupsToRelease;
1164 
1165     // We are examining each group and will take the minimum distance to the
1166     // release threshold as the next Region::TryReleaseThreshold(). Note that if
1167     // the size of free blocks has reached the release threshold, the distance
1168     // to the next release will be PageSize * SmallerBlockReleasePageDelta. See
1169     // the comment on `SmallerBlockReleasePageDelta` for more details.
1170     uptr MinDistToThreshold = GroupSize;
1171 
1172     for (BatchGroup *BG = Region->FreeListInfo.BlockList.front(),
1173                     *Prev = nullptr;
1174          BG != nullptr;) {
1175       // Group boundary is always GroupSize-aligned from CompactPtr base. The
1176       // layout of memory groups is like,
1177       //
1178       //     (CompactPtrBase)
1179       // #1 CompactPtrGroupBase   #2 CompactPtrGroupBase            ...
1180       //           |                       |                       |
1181       //           v                       v                       v
1182       //           +-----------------------+-----------------------+
1183       //            \                     / \                     /
1184       //             ---   GroupSize   ---   ---   GroupSize   ---
1185       //
1186       // After decompacting the CompactPtrGroupBase, we expect the alignment
1187       // property is held as well.
1188       const uptr BatchGroupBase =
1189           decompactGroupBase(CompactPtrBase, BG->CompactPtrGroupBase);
1190       DCHECK_LE(Region->RegionBeg, BatchGroupBase);
1191       DCHECK_GE(AllocatedUserEnd, BatchGroupBase);
1192       DCHECK_EQ((Region->RegionBeg - BatchGroupBase) % GroupSize, 0U);
1193       // TransferBatches are pushed in front of BG.Batches. The first one may
1194       // not have all caches used.
1195       const uptr NumBlocks = (BG->Batches.size() - 1) * BG->MaxCachedPerBatch +
1196                              BG->Batches.front()->getCount();
1197       const uptr BytesInBG = NumBlocks * BlockSize;
1198 
1199       if (BytesInBG <= BG->BytesInBGAtLastCheckpoint) {
1200         BG->BytesInBGAtLastCheckpoint = BytesInBG;
1201         Prev = BG;
1202         BG = BG->Next;
1203         continue;
1204       }
1205 
1206       const uptr PushedBytesDelta = BG->BytesInBGAtLastCheckpoint - BytesInBG;
1207 
1208       // Given the randomness property, we try to release the pages only if the
1209       // bytes used by free blocks exceed certain proportion of group size. Note
1210       // that this heuristic only applies when all the spaces in a BatchGroup
1211       // are allocated.
1212       if (isSmallBlock(BlockSize)) {
1213         const uptr BatchGroupEnd = BatchGroupBase + GroupSize;
1214         const uptr AllocatedGroupSize = AllocatedUserEnd >= BatchGroupEnd
1215                                             ? GroupSize
1216                                             : AllocatedUserEnd - BatchGroupBase;
1217         const uptr ReleaseThreshold =
1218             (AllocatedGroupSize * (100 - 1U - BlockSize / 16U)) / 100U;
1219         const bool HighDensity = BytesInBG >= ReleaseThreshold;
1220         const bool MayHaveReleasedAll = NumBlocks >= (GroupSize / BlockSize);
1221         // If all blocks in the group are released, we will do range marking
1222         // which is fast. Otherwise, we will wait until we have accumulated
1223         // a certain amount of free memory.
1224         const bool ReachReleaseDelta =
1225             MayHaveReleasedAll
1226                 ? true
1227                 : PushedBytesDelta >= PageSize * SmallerBlockReleasePageDelta;
1228 
1229         if (!HighDensity) {
1230           DCHECK_LE(BytesInBG, ReleaseThreshold);
1231           // The following is the usage of a memroy group,
1232           //
1233           //     BytesInBG             ReleaseThreshold
1234           //  /             \                 v
1235           //  +---+---------------------------+-----+
1236           //  |   |         |                 |     |
1237           //  +---+---------------------------+-----+
1238           //       \        /                       ^
1239           //    PushedBytesDelta                 GroupEnd
1240           MinDistToThreshold =
1241               Min(MinDistToThreshold,
1242                   ReleaseThreshold - BytesInBG + PushedBytesDelta);
1243         } else {
1244           // If it reaches high density at this round, the next time we will try
1245           // to release is based on SmallerBlockReleasePageDelta
1246           MinDistToThreshold =
1247               Min(MinDistToThreshold, PageSize * SmallerBlockReleasePageDelta);
1248         }
1249 
1250         if (!HighDensity || !ReachReleaseDelta) {
1251           Prev = BG;
1252           BG = BG->Next;
1253           continue;
1254         }
1255       }
1256 
1257       // If `BG` is the first BatchGroup in the list, we only need to advance
1258       // `BG` and call FreeListInfo.BlockList::pop_front(). No update is needed
1259       // for `Prev`.
1260       //
1261       //         (BG)   (BG->Next)
1262       // Prev     Cur      BG
1263       //   |       |       |
1264       //   v       v       v
1265       //  nil     +--+    +--+
1266       //          |X | -> |  | -> ...
1267       //          +--+    +--+
1268       //
1269       // Otherwise, `Prev` will be used to extract the `Cur` from the
1270       // `FreeListInfo.BlockList`.
1271       //
1272       //         (BG)   (BG->Next)
1273       // Prev     Cur      BG
1274       //   |       |       |
1275       //   v       v       v
1276       //  +--+    +--+    +--+
1277       //  |  | -> |X | -> |  | -> ...
1278       //  +--+    +--+    +--+
1279       //
1280       // After FreeListInfo.BlockList::extract(),
1281       //
1282       // Prev     Cur       BG
1283       //   |       |        |
1284       //   v       v        v
1285       //  +--+    +--+     +--+
1286       //  |  |-+  |X |  +->|  | -> ...
1287       //  +--+ |  +--+  |  +--+
1288       //       +--------+
1289       //
1290       // Note that we need to advance before pushing this BatchGroup to
1291       // GroupsToRelease because it's a destructive operation.
1292 
1293       BatchGroup *Cur = BG;
1294       BG = BG->Next;
1295 
1296       // Ideally, we may want to update this only after successful release.
1297       // However, for smaller blocks, each block marking is a costly operation.
1298       // Therefore, we update it earlier.
1299       // TODO: Consider updating this after releasing pages if `ReleaseRecorder`
1300       // can tell the released bytes in each group.
1301       Cur->BytesInBGAtLastCheckpoint = BytesInBG;
1302 
1303       if (Prev != nullptr)
1304         Region->FreeListInfo.BlockList.extract(Prev, Cur);
1305       else
1306         Region->FreeListInfo.BlockList.pop_front();
1307       GroupsToRelease.push_back(Cur);
1308     }
1309 
1310     // Only small blocks have the adaptive `TryReleaseThreshold`.
1311     if (isSmallBlock(BlockSize)) {
1312       // If the MinDistToThreshold is not updated, that means each memory group
1313       // may have only pushed less than a page size. In that case, just set it
1314       // back to normal.
1315       if (MinDistToThreshold == GroupSize)
1316         MinDistToThreshold = PageSize * SmallerBlockReleasePageDelta;
1317       Region->TryReleaseThreshold = MinDistToThreshold;
1318     }
1319 
1320     return GroupsToRelease;
1321   }
1322 
1323   PageReleaseContext
1324   markFreeBlocks(RegionInfo *Region, const uptr BlockSize,
1325                  const uptr AllocatedUserEnd, const uptr CompactPtrBase,
1326                  SinglyLinkedList<BatchGroup> &GroupsToRelease)
1327       REQUIRES(Region->MMLock) EXCLUDES(Region->FLLock) {
1328     const uptr GroupSize = (1U << GroupSizeLog);
1329     auto DecompactPtr = [CompactPtrBase](CompactPtrT CompactPtr) {
1330       return decompactPtrInternal(CompactPtrBase, CompactPtr);
1331     };
1332 
1333     const uptr ReleaseBase = decompactGroupBase(
1334         CompactPtrBase, GroupsToRelease.front()->CompactPtrGroupBase);
1335     const uptr LastGroupEnd =
1336         Min(decompactGroupBase(CompactPtrBase,
1337                                GroupsToRelease.back()->CompactPtrGroupBase) +
1338                 GroupSize,
1339             AllocatedUserEnd);
1340     // The last block may straddle the group boundary. Rounding up to BlockSize
1341     // to get the exact range.
1342     const uptr ReleaseEnd =
1343         roundUpSlow(LastGroupEnd - Region->RegionBeg, BlockSize) +
1344         Region->RegionBeg;
1345     const uptr ReleaseRangeSize = ReleaseEnd - ReleaseBase;
1346     const uptr ReleaseOffset = ReleaseBase - Region->RegionBeg;
1347 
1348     PageReleaseContext Context(BlockSize, /*NumberOfRegions=*/1U,
1349                                ReleaseRangeSize, ReleaseOffset);
1350     // We may not be able to do the page release in a rare case that we may
1351     // fail on PageMap allocation.
1352     if (UNLIKELY(!Context.ensurePageMapAllocated()))
1353       return Context;
1354 
1355     for (BatchGroup &BG : GroupsToRelease) {
1356       const uptr BatchGroupBase =
1357           decompactGroupBase(CompactPtrBase, BG.CompactPtrGroupBase);
1358       const uptr BatchGroupEnd = BatchGroupBase + GroupSize;
1359       const uptr AllocatedGroupSize = AllocatedUserEnd >= BatchGroupEnd
1360                                           ? GroupSize
1361                                           : AllocatedUserEnd - BatchGroupBase;
1362       const uptr BatchGroupUsedEnd = BatchGroupBase + AllocatedGroupSize;
1363       const bool MayContainLastBlockInRegion =
1364           BatchGroupUsedEnd == AllocatedUserEnd;
1365       const bool BlockAlignedWithUsedEnd =
1366           (BatchGroupUsedEnd - Region->RegionBeg) % BlockSize == 0;
1367 
1368       uptr MaxContainedBlocks = AllocatedGroupSize / BlockSize;
1369       if (!BlockAlignedWithUsedEnd)
1370         ++MaxContainedBlocks;
1371 
1372       const uptr NumBlocks = (BG.Batches.size() - 1) * BG.MaxCachedPerBatch +
1373                              BG.Batches.front()->getCount();
1374 
1375       if (NumBlocks == MaxContainedBlocks) {
1376         for (const auto &It : BG.Batches) {
1377           if (&It != BG.Batches.front())
1378             DCHECK_EQ(It.getCount(), BG.MaxCachedPerBatch);
1379           for (u16 I = 0; I < It.getCount(); ++I)
1380             DCHECK_EQ(compactPtrGroup(It.get(I)), BG.CompactPtrGroupBase);
1381         }
1382 
1383         Context.markRangeAsAllCounted(BatchGroupBase, BatchGroupUsedEnd,
1384                                       Region->RegionBeg, /*RegionIndex=*/0,
1385                                       Region->MemMapInfo.AllocatedUser);
1386       } else {
1387         DCHECK_LT(NumBlocks, MaxContainedBlocks);
1388         // Note that we don't always visit blocks in each BatchGroup so that we
1389         // may miss the chance of releasing certain pages that cross
1390         // BatchGroups.
1391         Context.markFreeBlocksInRegion(
1392             BG.Batches, DecompactPtr, Region->RegionBeg, /*RegionIndex=*/0,
1393             Region->MemMapInfo.AllocatedUser, MayContainLastBlockInRegion);
1394       }
1395     }
1396 
1397     DCHECK(Context.hasBlockMarked());
1398 
1399     return Context;
1400   }
1401 
1402   void mergeGroupsToReleaseBack(RegionInfo *Region,
1403                                 SinglyLinkedList<BatchGroup> &GroupsToRelease)
1404       REQUIRES(Region->MMLock, Region->FLLock) {
1405     // After merging two freelists, we may have redundant `BatchGroup`s that
1406     // need to be recycled. The number of unused `BatchGroup`s is expected to be
1407     // small. Pick a constant which is inferred from real programs.
1408     constexpr uptr MaxUnusedSize = 8;
1409     CompactPtrT Blocks[MaxUnusedSize];
1410     u32 Idx = 0;
1411     RegionInfo *BatchClassRegion = getRegionInfo(SizeClassMap::BatchClassId);
1412     // We can't call pushBatchClassBlocks() to recycle the unused `BatchGroup`s
1413     // when we are manipulating the freelist of `BatchClassRegion`. Instead, we
1414     // should just push it back to the freelist when we merge two `BatchGroup`s.
1415     // This logic hasn't been implemented because we haven't supported releasing
1416     // pages in `BatchClassRegion`.
1417     DCHECK_NE(BatchClassRegion, Region);
1418 
1419     // Merge GroupsToRelease back to the Region::FreeListInfo.BlockList. Note
1420     // that both `Region->FreeListInfo.BlockList` and `GroupsToRelease` are
1421     // sorted.
1422     for (BatchGroup *BG = Region->FreeListInfo.BlockList.front(),
1423                     *Prev = nullptr;
1424          ;) {
1425       if (BG == nullptr || GroupsToRelease.empty()) {
1426         if (!GroupsToRelease.empty())
1427           Region->FreeListInfo.BlockList.append_back(&GroupsToRelease);
1428         break;
1429       }
1430 
1431       DCHECK(!BG->Batches.empty());
1432 
1433       if (BG->CompactPtrGroupBase <
1434           GroupsToRelease.front()->CompactPtrGroupBase) {
1435         Prev = BG;
1436         BG = BG->Next;
1437         continue;
1438       }
1439 
1440       BatchGroup *Cur = GroupsToRelease.front();
1441       TransferBatch *UnusedTransferBatch = nullptr;
1442       GroupsToRelease.pop_front();
1443 
1444       if (BG->CompactPtrGroupBase == Cur->CompactPtrGroupBase) {
1445         BG->PushedBlocks += Cur->PushedBlocks;
1446         // We have updated `BatchGroup::BytesInBGAtLastCheckpoint` while
1447         // collecting the `GroupsToRelease`.
1448         BG->BytesInBGAtLastCheckpoint = Cur->BytesInBGAtLastCheckpoint;
1449         const uptr MaxCachedPerBatch = BG->MaxCachedPerBatch;
1450 
1451         // Note that the first TransferBatches in both `Batches` may not be
1452         // full and only the first TransferBatch can have non-full blocks. Thus
1453         // we have to merge them before appending one to another.
1454         if (Cur->Batches.front()->getCount() == MaxCachedPerBatch) {
1455           BG->Batches.append_back(&Cur->Batches);
1456         } else {
1457           TransferBatch *NonFullBatch = Cur->Batches.front();
1458           Cur->Batches.pop_front();
1459           const u16 NonFullBatchCount = NonFullBatch->getCount();
1460           // The remaining Batches in `Cur` are full.
1461           BG->Batches.append_back(&Cur->Batches);
1462 
1463           if (BG->Batches.front()->getCount() == MaxCachedPerBatch) {
1464             // Only 1 non-full TransferBatch, push it to the front.
1465             BG->Batches.push_front(NonFullBatch);
1466           } else {
1467             const u16 NumBlocksToMove = static_cast<u16>(
1468                 Min(static_cast<u16>(MaxCachedPerBatch -
1469                                      BG->Batches.front()->getCount()),
1470                     NonFullBatchCount));
1471             BG->Batches.front()->appendFromTransferBatch(NonFullBatch,
1472                                                          NumBlocksToMove);
1473             if (NonFullBatch->isEmpty())
1474               UnusedTransferBatch = NonFullBatch;
1475             else
1476               BG->Batches.push_front(NonFullBatch);
1477           }
1478         }
1479 
1480         const u32 NeededSlots = UnusedTransferBatch == nullptr ? 1U : 2U;
1481         if (UNLIKELY(Idx + NeededSlots > MaxUnusedSize)) {
1482           ScopedLock L(BatchClassRegion->FLLock);
1483           pushBatchClassBlocks(BatchClassRegion, Blocks, Idx);
1484           Idx = 0;
1485         }
1486         Blocks[Idx++] =
1487             compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(Cur));
1488         if (UnusedTransferBatch) {
1489           Blocks[Idx++] =
1490               compactPtr(SizeClassMap::BatchClassId,
1491                          reinterpret_cast<uptr>(UnusedTransferBatch));
1492         }
1493         Prev = BG;
1494         BG = BG->Next;
1495         continue;
1496       }
1497 
1498       // At here, the `BG` is the first BatchGroup with CompactPtrGroupBase
1499       // larger than the first element in `GroupsToRelease`. We need to insert
1500       // `GroupsToRelease::front()` (which is `Cur` below)  before `BG`.
1501       //
1502       //   1. If `Prev` is nullptr, we simply push `Cur` to the front of
1503       //      FreeListInfo.BlockList.
1504       //   2. Otherwise, use `insert()` which inserts an element next to `Prev`.
1505       //
1506       // Afterwards, we don't need to advance `BG` because the order between
1507       // `BG` and the new `GroupsToRelease::front()` hasn't been checked.
1508       if (Prev == nullptr)
1509         Region->FreeListInfo.BlockList.push_front(Cur);
1510       else
1511         Region->FreeListInfo.BlockList.insert(Prev, Cur);
1512       DCHECK_EQ(Cur->Next, BG);
1513       Prev = Cur;
1514     }
1515 
1516     if (Idx != 0) {
1517       ScopedLock L(BatchClassRegion->FLLock);
1518       pushBatchClassBlocks(BatchClassRegion, Blocks, Idx);
1519     }
1520 
1521     if (SCUDO_DEBUG) {
1522       BatchGroup *Prev = Region->FreeListInfo.BlockList.front();
1523       for (BatchGroup *Cur = Prev->Next; Cur != nullptr;
1524            Prev = Cur, Cur = Cur->Next) {
1525         CHECK_LT(Prev->CompactPtrGroupBase, Cur->CompactPtrGroupBase);
1526       }
1527     }
1528   }
1529 
1530   // TODO: `PrimaryBase` can be obtained from ReservedMemory. This needs to be
1531   // deprecated.
1532   uptr PrimaryBase = 0;
1533   ReservedMemoryT ReservedMemory = {};
1534   // The minimum size of pushed blocks that we will try to release the pages in
1535   // that size class.
1536   uptr SmallerBlockReleasePageDelta = 0;
1537   atomic_s32 ReleaseToOsIntervalMs = {};
1538   alignas(SCUDO_CACHE_LINE_SIZE) RegionInfo RegionInfoArray[NumClasses];
1539 };
1540 
1541 } // namespace scudo
1542 
1543 #endif // SCUDO_PRIMARY64_H_
1544