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