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