Path: blob/main/contrib/llvm-project/compiler-rt/lib/scudo/standalone/primary32.h
35292 views
//===-- primary32.h ---------------------------------------------*- C++ -*-===//1//2// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.3// See https://llvm.org/LICENSE.txt for license information.4// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception5//6//===----------------------------------------------------------------------===//78#ifndef SCUDO_PRIMARY32_H_9#define SCUDO_PRIMARY32_H_1011#include "allocator_common.h"12#include "bytemap.h"13#include "common.h"14#include "list.h"15#include "local_cache.h"16#include "options.h"17#include "release.h"18#include "report.h"19#include "stats.h"20#include "string_utils.h"21#include "thread_annotations.h"2223namespace scudo {2425// SizeClassAllocator32 is an allocator for 32 or 64-bit address space.26//27// It maps Regions of 2^RegionSizeLog bytes aligned on a 2^RegionSizeLog bytes28// boundary, and keeps a bytemap of the mappable address space to track the size29// class they are associated with.30//31// Mapped regions are split into equally sized Blocks according to the size32// class they belong to, and the associated pointers are shuffled to prevent any33// predictable address pattern (the predictability increases with the block34// size).35//36// Regions for size class 0 are special and used to hold TransferBatches, which37// allow to transfer arrays of pointers from the global size class freelist to38// the thread specific freelist for said class, and back.39//40// Memory used by this allocator is never unmapped but can be partially41// reclaimed if the platform allows for it.4243template <typename Config> class SizeClassAllocator32 {44public:45typedef typename Config::CompactPtrT CompactPtrT;46typedef typename Config::SizeClassMap SizeClassMap;47static const uptr GroupSizeLog = Config::getGroupSizeLog();48// The bytemap can only track UINT8_MAX - 1 classes.49static_assert(SizeClassMap::LargestClassId <= (UINT8_MAX - 1), "");50// Regions should be large enough to hold the largest Block.51static_assert((1UL << Config::getRegionSizeLog()) >= SizeClassMap::MaxSize,52"");53typedef SizeClassAllocator32<Config> ThisT;54typedef SizeClassAllocatorLocalCache<ThisT> CacheT;55typedef TransferBatch<ThisT> TransferBatchT;56typedef BatchGroup<ThisT> BatchGroupT;5758static_assert(sizeof(BatchGroupT) <= sizeof(TransferBatchT),59"BatchGroupT uses the same class size as TransferBatchT");6061static uptr getSizeByClassId(uptr ClassId) {62return (ClassId == SizeClassMap::BatchClassId)63? sizeof(TransferBatchT)64: SizeClassMap::getSizeByClassId(ClassId);65}6667static bool canAllocate(uptr Size) { return Size <= SizeClassMap::MaxSize; }6869void init(s32 ReleaseToOsInterval) NO_THREAD_SAFETY_ANALYSIS {70if (SCUDO_FUCHSIA)71reportError("SizeClassAllocator32 is not supported on Fuchsia");7273if (SCUDO_TRUSTY)74reportError("SizeClassAllocator32 is not supported on Trusty");7576DCHECK(isAligned(reinterpret_cast<uptr>(this), alignof(ThisT)));77PossibleRegions.init();78u32 Seed;79const u64 Time = getMonotonicTimeFast();80if (!getRandom(reinterpret_cast<void *>(&Seed), sizeof(Seed)))81Seed = static_cast<u32>(82Time ^ (reinterpret_cast<uptr>(SizeClassInfoArray) >> 6));83for (uptr I = 0; I < NumClasses; I++) {84SizeClassInfo *Sci = getSizeClassInfo(I);85Sci->RandState = getRandomU32(&Seed);86// Sci->MaxRegionIndex is already initialized to 0.87Sci->MinRegionIndex = NumRegions;88Sci->ReleaseInfo.LastReleaseAtNs = Time;89}9091// The default value in the primary config has the higher priority.92if (Config::getDefaultReleaseToOsIntervalMs() != INT32_MIN)93ReleaseToOsInterval = Config::getDefaultReleaseToOsIntervalMs();94setOption(Option::ReleaseInterval, static_cast<sptr>(ReleaseToOsInterval));95}9697void unmapTestOnly() {98{99ScopedLock L(RegionsStashMutex);100while (NumberOfStashedRegions > 0) {101unmap(reinterpret_cast<void *>(RegionsStash[--NumberOfStashedRegions]),102RegionSize);103}104}105106uptr MinRegionIndex = NumRegions, MaxRegionIndex = 0;107for (uptr I = 0; I < NumClasses; I++) {108SizeClassInfo *Sci = getSizeClassInfo(I);109ScopedLock L(Sci->Mutex);110if (Sci->MinRegionIndex < MinRegionIndex)111MinRegionIndex = Sci->MinRegionIndex;112if (Sci->MaxRegionIndex > MaxRegionIndex)113MaxRegionIndex = Sci->MaxRegionIndex;114*Sci = {};115}116117ScopedLock L(ByteMapMutex);118for (uptr I = MinRegionIndex; I <= MaxRegionIndex; I++)119if (PossibleRegions[I])120unmap(reinterpret_cast<void *>(I * RegionSize), RegionSize);121PossibleRegions.unmapTestOnly();122}123124// When all blocks are freed, it has to be the same size as `AllocatedUser`.125void verifyAllBlocksAreReleasedTestOnly() {126// `BatchGroup` and `TransferBatch` also use the blocks from BatchClass.127uptr BatchClassUsedInFreeLists = 0;128for (uptr I = 0; I < NumClasses; I++) {129// We have to count BatchClassUsedInFreeLists in other regions first.130if (I == SizeClassMap::BatchClassId)131continue;132SizeClassInfo *Sci = getSizeClassInfo(I);133ScopedLock L1(Sci->Mutex);134uptr TotalBlocks = 0;135for (BatchGroupT &BG : Sci->FreeListInfo.BlockList) {136// `BG::Batches` are `TransferBatches`. +1 for `BatchGroup`.137BatchClassUsedInFreeLists += BG.Batches.size() + 1;138for (const auto &It : BG.Batches)139TotalBlocks += It.getCount();140}141142const uptr BlockSize = getSizeByClassId(I);143DCHECK_EQ(TotalBlocks, Sci->AllocatedUser / BlockSize);144DCHECK_EQ(Sci->FreeListInfo.PushedBlocks, Sci->FreeListInfo.PoppedBlocks);145}146147SizeClassInfo *Sci = getSizeClassInfo(SizeClassMap::BatchClassId);148ScopedLock L1(Sci->Mutex);149uptr TotalBlocks = 0;150for (BatchGroupT &BG : Sci->FreeListInfo.BlockList) {151if (LIKELY(!BG.Batches.empty())) {152for (const auto &It : BG.Batches)153TotalBlocks += It.getCount();154} else {155// `BatchGroup` with empty freelist doesn't have `TransferBatch` record156// itself.157++TotalBlocks;158}159}160161const uptr BlockSize = getSizeByClassId(SizeClassMap::BatchClassId);162DCHECK_EQ(TotalBlocks + BatchClassUsedInFreeLists,163Sci->AllocatedUser / BlockSize);164const uptr BlocksInUse =165Sci->FreeListInfo.PoppedBlocks - Sci->FreeListInfo.PushedBlocks;166DCHECK_EQ(BlocksInUse, BatchClassUsedInFreeLists);167}168169CompactPtrT compactPtr(UNUSED uptr ClassId, uptr Ptr) const {170return static_cast<CompactPtrT>(Ptr);171}172173void *decompactPtr(UNUSED uptr ClassId, CompactPtrT CompactPtr) const {174return reinterpret_cast<void *>(static_cast<uptr>(CompactPtr));175}176177uptr compactPtrGroupBase(CompactPtrT CompactPtr) {178const uptr Mask = (static_cast<uptr>(1) << GroupSizeLog) - 1;179return CompactPtr & ~Mask;180}181182uptr decompactGroupBase(uptr CompactPtrGroupBase) {183return CompactPtrGroupBase;184}185186ALWAYS_INLINE static bool isSmallBlock(uptr BlockSize) {187const uptr PageSize = getPageSizeCached();188return BlockSize < PageSize / 16U;189}190191ALWAYS_INLINE static bool isLargeBlock(uptr BlockSize) {192const uptr PageSize = getPageSizeCached();193return BlockSize > PageSize;194}195196u16 popBlocks(CacheT *C, uptr ClassId, CompactPtrT *ToArray,197const u16 MaxBlockCount) {198DCHECK_LT(ClassId, NumClasses);199SizeClassInfo *Sci = getSizeClassInfo(ClassId);200ScopedLock L(Sci->Mutex);201202u16 PopCount = popBlocksImpl(C, ClassId, Sci, ToArray, MaxBlockCount);203if (UNLIKELY(PopCount == 0)) {204if (UNLIKELY(!populateFreeList(C, ClassId, Sci)))205return 0U;206PopCount = popBlocksImpl(C, ClassId, Sci, ToArray, MaxBlockCount);207DCHECK_NE(PopCount, 0U);208}209210return PopCount;211}212213// Push the array of free blocks to the designated batch group.214void pushBlocks(CacheT *C, uptr ClassId, CompactPtrT *Array, u32 Size) {215DCHECK_LT(ClassId, NumClasses);216DCHECK_GT(Size, 0);217218SizeClassInfo *Sci = getSizeClassInfo(ClassId);219if (ClassId == SizeClassMap::BatchClassId) {220ScopedLock L(Sci->Mutex);221pushBatchClassBlocks(Sci, Array, Size);222return;223}224225// TODO(chiahungduan): Consider not doing grouping if the group size is not226// greater than the block size with a certain scale.227228// Sort the blocks so that blocks belonging to the same group can be pushed229// together.230bool SameGroup = true;231for (u32 I = 1; I < Size; ++I) {232if (compactPtrGroupBase(Array[I - 1]) != compactPtrGroupBase(Array[I]))233SameGroup = false;234CompactPtrT Cur = Array[I];235u32 J = I;236while (J > 0 &&237compactPtrGroupBase(Cur) < compactPtrGroupBase(Array[J - 1])) {238Array[J] = Array[J - 1];239--J;240}241Array[J] = Cur;242}243244ScopedLock L(Sci->Mutex);245pushBlocksImpl(C, ClassId, Sci, Array, Size, SameGroup);246}247248void disable() NO_THREAD_SAFETY_ANALYSIS {249// The BatchClassId must be locked last since other classes can use it.250for (sptr I = static_cast<sptr>(NumClasses) - 1; I >= 0; I--) {251if (static_cast<uptr>(I) == SizeClassMap::BatchClassId)252continue;253getSizeClassInfo(static_cast<uptr>(I))->Mutex.lock();254}255getSizeClassInfo(SizeClassMap::BatchClassId)->Mutex.lock();256RegionsStashMutex.lock();257ByteMapMutex.lock();258}259260void enable() NO_THREAD_SAFETY_ANALYSIS {261ByteMapMutex.unlock();262RegionsStashMutex.unlock();263getSizeClassInfo(SizeClassMap::BatchClassId)->Mutex.unlock();264for (uptr I = 0; I < NumClasses; I++) {265if (I == SizeClassMap::BatchClassId)266continue;267getSizeClassInfo(I)->Mutex.unlock();268}269}270271template <typename F> void iterateOverBlocks(F Callback) {272uptr MinRegionIndex = NumRegions, MaxRegionIndex = 0;273for (uptr I = 0; I < NumClasses; I++) {274SizeClassInfo *Sci = getSizeClassInfo(I);275// TODO: The call of `iterateOverBlocks` requires disabling276// SizeClassAllocator32. We may consider locking each region on demand277// only.278Sci->Mutex.assertHeld();279if (Sci->MinRegionIndex < MinRegionIndex)280MinRegionIndex = Sci->MinRegionIndex;281if (Sci->MaxRegionIndex > MaxRegionIndex)282MaxRegionIndex = Sci->MaxRegionIndex;283}284285// SizeClassAllocator32 is disabled, i.e., ByteMapMutex is held.286ByteMapMutex.assertHeld();287288for (uptr I = MinRegionIndex; I <= MaxRegionIndex; I++) {289if (PossibleRegions[I] &&290(PossibleRegions[I] - 1U) != SizeClassMap::BatchClassId) {291const uptr BlockSize = getSizeByClassId(PossibleRegions[I] - 1U);292const uptr From = I * RegionSize;293const uptr To = From + (RegionSize / BlockSize) * BlockSize;294for (uptr Block = From; Block < To; Block += BlockSize)295Callback(Block);296}297}298}299300void getStats(ScopedString *Str) {301// TODO(kostyak): get the RSS per region.302uptr TotalMapped = 0;303uptr PoppedBlocks = 0;304uptr PushedBlocks = 0;305for (uptr I = 0; I < NumClasses; I++) {306SizeClassInfo *Sci = getSizeClassInfo(I);307ScopedLock L(Sci->Mutex);308TotalMapped += Sci->AllocatedUser;309PoppedBlocks += Sci->FreeListInfo.PoppedBlocks;310PushedBlocks += Sci->FreeListInfo.PushedBlocks;311}312Str->append("Stats: SizeClassAllocator32: %zuM mapped in %zu allocations; "313"remains %zu\n",314TotalMapped >> 20, PoppedBlocks, PoppedBlocks - PushedBlocks);315for (uptr I = 0; I < NumClasses; I++) {316SizeClassInfo *Sci = getSizeClassInfo(I);317ScopedLock L(Sci->Mutex);318getStats(Str, I, Sci);319}320}321322void getFragmentationInfo(ScopedString *Str) {323Str->append(324"Fragmentation Stats: SizeClassAllocator32: page size = %zu bytes\n",325getPageSizeCached());326327for (uptr I = 1; I < NumClasses; I++) {328SizeClassInfo *Sci = getSizeClassInfo(I);329ScopedLock L(Sci->Mutex);330getSizeClassFragmentationInfo(Sci, I, Str);331}332}333334bool setOption(Option O, sptr Value) {335if (O == Option::ReleaseInterval) {336const s32 Interval = Max(337Min(static_cast<s32>(Value), Config::getMaxReleaseToOsIntervalMs()),338Config::getMinReleaseToOsIntervalMs());339atomic_store_relaxed(&ReleaseToOsIntervalMs, Interval);340return true;341}342// Not supported by the Primary, but not an error either.343return true;344}345346uptr tryReleaseToOS(uptr ClassId, ReleaseToOS ReleaseType) {347SizeClassInfo *Sci = getSizeClassInfo(ClassId);348// TODO: Once we have separate locks like primary64, we may consider using349// tryLock() as well.350ScopedLock L(Sci->Mutex);351return releaseToOSMaybe(Sci, ClassId, ReleaseType);352}353354uptr releaseToOS(ReleaseToOS ReleaseType) {355uptr TotalReleasedBytes = 0;356for (uptr I = 0; I < NumClasses; I++) {357if (I == SizeClassMap::BatchClassId)358continue;359SizeClassInfo *Sci = getSizeClassInfo(I);360ScopedLock L(Sci->Mutex);361TotalReleasedBytes += releaseToOSMaybe(Sci, I, ReleaseType);362}363return TotalReleasedBytes;364}365366const char *getRegionInfoArrayAddress() const { return nullptr; }367static uptr getRegionInfoArraySize() { return 0; }368369static BlockInfo findNearestBlock(UNUSED const char *RegionInfoData,370UNUSED uptr Ptr) {371return {};372}373374AtomicOptions Options;375376private:377static const uptr NumClasses = SizeClassMap::NumClasses;378static const uptr RegionSize = 1UL << Config::getRegionSizeLog();379static const uptr NumRegions = SCUDO_MMAP_RANGE_SIZE >>380Config::getRegionSizeLog();381static const u32 MaxNumBatches = SCUDO_ANDROID ? 4U : 8U;382typedef FlatByteMap<NumRegions> ByteMap;383384struct ReleaseToOsInfo {385uptr BytesInFreeListAtLastCheckpoint;386uptr RangesReleased;387uptr LastReleasedBytes;388u64 LastReleaseAtNs;389};390391struct BlocksInfo {392SinglyLinkedList<BatchGroupT> BlockList = {};393uptr PoppedBlocks = 0;394uptr PushedBlocks = 0;395};396397struct alignas(SCUDO_CACHE_LINE_SIZE) SizeClassInfo {398HybridMutex Mutex;399BlocksInfo FreeListInfo GUARDED_BY(Mutex);400uptr CurrentRegion GUARDED_BY(Mutex);401uptr CurrentRegionAllocated GUARDED_BY(Mutex);402u32 RandState;403uptr AllocatedUser GUARDED_BY(Mutex);404// Lowest & highest region index allocated for this size class, to avoid405// looping through the whole NumRegions.406uptr MinRegionIndex GUARDED_BY(Mutex);407uptr MaxRegionIndex GUARDED_BY(Mutex);408ReleaseToOsInfo ReleaseInfo GUARDED_BY(Mutex);409};410static_assert(sizeof(SizeClassInfo) % SCUDO_CACHE_LINE_SIZE == 0, "");411412uptr computeRegionId(uptr Mem) {413const uptr Id = Mem >> Config::getRegionSizeLog();414CHECK_LT(Id, NumRegions);415return Id;416}417418uptr allocateRegionSlow() {419uptr MapSize = 2 * RegionSize;420const uptr MapBase = reinterpret_cast<uptr>(421map(nullptr, MapSize, "scudo:primary", MAP_ALLOWNOMEM));422if (!MapBase)423return 0;424const uptr MapEnd = MapBase + MapSize;425uptr Region = MapBase;426if (isAligned(Region, RegionSize)) {427ScopedLock L(RegionsStashMutex);428if (NumberOfStashedRegions < MaxStashedRegions)429RegionsStash[NumberOfStashedRegions++] = MapBase + RegionSize;430else431MapSize = RegionSize;432} else {433Region = roundUp(MapBase, RegionSize);434unmap(reinterpret_cast<void *>(MapBase), Region - MapBase);435MapSize = RegionSize;436}437const uptr End = Region + MapSize;438if (End != MapEnd)439unmap(reinterpret_cast<void *>(End), MapEnd - End);440441DCHECK_EQ(Region % RegionSize, 0U);442static_assert(Config::getRegionSizeLog() == GroupSizeLog,443"Memory group should be the same size as Region");444445return Region;446}447448uptr allocateRegion(SizeClassInfo *Sci, uptr ClassId) REQUIRES(Sci->Mutex) {449DCHECK_LT(ClassId, NumClasses);450uptr Region = 0;451{452ScopedLock L(RegionsStashMutex);453if (NumberOfStashedRegions > 0)454Region = RegionsStash[--NumberOfStashedRegions];455}456if (!Region)457Region = allocateRegionSlow();458if (LIKELY(Region)) {459// Sci->Mutex is held by the caller, updating the Min/Max is safe.460const uptr RegionIndex = computeRegionId(Region);461if (RegionIndex < Sci->MinRegionIndex)462Sci->MinRegionIndex = RegionIndex;463if (RegionIndex > Sci->MaxRegionIndex)464Sci->MaxRegionIndex = RegionIndex;465ScopedLock L(ByteMapMutex);466PossibleRegions.set(RegionIndex, static_cast<u8>(ClassId + 1U));467}468return Region;469}470471SizeClassInfo *getSizeClassInfo(uptr ClassId) {472DCHECK_LT(ClassId, NumClasses);473return &SizeClassInfoArray[ClassId];474}475476void pushBatchClassBlocks(SizeClassInfo *Sci, CompactPtrT *Array, u32 Size)477REQUIRES(Sci->Mutex) {478DCHECK_EQ(Sci, getSizeClassInfo(SizeClassMap::BatchClassId));479480// Free blocks are recorded by TransferBatch in freelist for all481// size-classes. In addition, TransferBatch is allocated from BatchClassId.482// In order not to use additional block to record the free blocks in483// BatchClassId, they are self-contained. I.e., A TransferBatch records the484// block address of itself. See the figure below:485//486// TransferBatch at 0xABCD487// +----------------------------+488// | Free blocks' addr |489// | +------+------+------+ |490// | |0xABCD|... |... | |491// | +------+------+------+ |492// +----------------------------+493//494// When we allocate all the free blocks in the TransferBatch, the block used495// by TransferBatch is also free for use. We don't need to recycle the496// TransferBatch. Note that the correctness is maintained by the invariant,497//498// Each popBlocks() request returns the entire TransferBatch. Returning499// part of the blocks in a TransferBatch is invalid.500//501// This ensures that TransferBatch won't leak the address itself while it's502// still holding other valid data.503//504// Besides, BatchGroup is also allocated from BatchClassId and has its505// address recorded in the TransferBatch too. To maintain the correctness,506//507// The address of BatchGroup is always recorded in the last TransferBatch508// in the freelist (also imply that the freelist should only be509// updated with push_front). Once the last TransferBatch is popped,510// the block used by BatchGroup is also free for use.511//512// With this approach, the blocks used by BatchGroup and TransferBatch are513// reusable and don't need additional space for them.514515Sci->FreeListInfo.PushedBlocks += Size;516BatchGroupT *BG = Sci->FreeListInfo.BlockList.front();517518if (BG == nullptr) {519// Construct `BatchGroup` on the last element.520BG = reinterpret_cast<BatchGroupT *>(521decompactPtr(SizeClassMap::BatchClassId, Array[Size - 1]));522--Size;523BG->Batches.clear();524// BatchClass hasn't enabled memory group. Use `0` to indicate there's no525// memory group here.526BG->CompactPtrGroupBase = 0;527// `BG` is also the block of BatchClassId. Note that this is different528// from `CreateGroup` in `pushBlocksImpl`529BG->PushedBlocks = 1;530BG->BytesInBGAtLastCheckpoint = 0;531BG->MaxCachedPerBatch =532CacheT::getMaxCached(getSizeByClassId(SizeClassMap::BatchClassId));533534Sci->FreeListInfo.BlockList.push_front(BG);535}536537if (UNLIKELY(Size == 0))538return;539540// This happens under 2 cases.541// 1. just allocated a new `BatchGroup`.542// 2. Only 1 block is pushed when the freelist is empty.543if (BG->Batches.empty()) {544// Construct the `TransferBatch` on the last element.545TransferBatchT *TB = reinterpret_cast<TransferBatchT *>(546decompactPtr(SizeClassMap::BatchClassId, Array[Size - 1]));547TB->clear();548// As mentioned above, addresses of `TransferBatch` and `BatchGroup` are549// recorded in the TransferBatch.550TB->add(Array[Size - 1]);551TB->add(552compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(BG)));553--Size;554DCHECK_EQ(BG->PushedBlocks, 1U);555// `TB` is also the block of BatchClassId.556BG->PushedBlocks += 1;557BG->Batches.push_front(TB);558}559560TransferBatchT *CurBatch = BG->Batches.front();561DCHECK_NE(CurBatch, nullptr);562563for (u32 I = 0; I < Size;) {564u16 UnusedSlots =565static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount());566if (UnusedSlots == 0) {567CurBatch = reinterpret_cast<TransferBatchT *>(568decompactPtr(SizeClassMap::BatchClassId, Array[I]));569CurBatch->clear();570// Self-contained571CurBatch->add(Array[I]);572++I;573// TODO(chiahungduan): Avoid the use of push_back() in `Batches` of574// BatchClassId.575BG->Batches.push_front(CurBatch);576UnusedSlots = static_cast<u16>(BG->MaxCachedPerBatch - 1);577}578// `UnusedSlots` is u16 so the result will be also fit in u16.579const u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I));580CurBatch->appendFromArray(&Array[I], AppendSize);581I += AppendSize;582}583584BG->PushedBlocks += Size;585}586// Push the blocks to their batch group. The layout will be like,587//588// FreeListInfo.BlockList - > BG -> BG -> BG589// | | |590// v v v591// TB TB TB592// |593// v594// TB595//596// Each BlockGroup(BG) will associate with unique group id and the free blocks597// are managed by a list of TransferBatch(TB). To reduce the time of inserting598// blocks, BGs are sorted and the input `Array` are supposed to be sorted so599// that we can get better performance of maintaining sorted property.600// Use `SameGroup=true` to indicate that all blocks in the array are from the601// same group then we will skip checking the group id of each block.602//603// The region mutex needs to be held while calling this method.604void pushBlocksImpl(CacheT *C, uptr ClassId, SizeClassInfo *Sci,605CompactPtrT *Array, u32 Size, bool SameGroup = false)606REQUIRES(Sci->Mutex) {607DCHECK_NE(ClassId, SizeClassMap::BatchClassId);608DCHECK_GT(Size, 0U);609610auto CreateGroup = [&](uptr CompactPtrGroupBase) {611BatchGroupT *BG =612reinterpret_cast<BatchGroupT *>(C->getBatchClassBlock());613BG->Batches.clear();614TransferBatchT *TB =615reinterpret_cast<TransferBatchT *>(C->getBatchClassBlock());616TB->clear();617618BG->CompactPtrGroupBase = CompactPtrGroupBase;619BG->Batches.push_front(TB);620BG->PushedBlocks = 0;621BG->BytesInBGAtLastCheckpoint = 0;622BG->MaxCachedPerBatch = TransferBatchT::MaxNumCached;623624return BG;625};626627auto InsertBlocks = [&](BatchGroupT *BG, CompactPtrT *Array, u32 Size) {628SinglyLinkedList<TransferBatchT> &Batches = BG->Batches;629TransferBatchT *CurBatch = Batches.front();630DCHECK_NE(CurBatch, nullptr);631632for (u32 I = 0; I < Size;) {633DCHECK_GE(BG->MaxCachedPerBatch, CurBatch->getCount());634u16 UnusedSlots =635static_cast<u16>(BG->MaxCachedPerBatch - CurBatch->getCount());636if (UnusedSlots == 0) {637CurBatch =638reinterpret_cast<TransferBatchT *>(C->getBatchClassBlock());639CurBatch->clear();640Batches.push_front(CurBatch);641UnusedSlots = BG->MaxCachedPerBatch;642}643// `UnusedSlots` is u16 so the result will be also fit in u16.644u16 AppendSize = static_cast<u16>(Min<u32>(UnusedSlots, Size - I));645CurBatch->appendFromArray(&Array[I], AppendSize);646I += AppendSize;647}648649BG->PushedBlocks += Size;650};651652Sci->FreeListInfo.PushedBlocks += Size;653BatchGroupT *Cur = Sci->FreeListInfo.BlockList.front();654655// In the following, `Cur` always points to the BatchGroup for blocks that656// will be pushed next. `Prev` is the element right before `Cur`.657BatchGroupT *Prev = nullptr;658659while (Cur != nullptr &&660compactPtrGroupBase(Array[0]) > Cur->CompactPtrGroupBase) {661Prev = Cur;662Cur = Cur->Next;663}664665if (Cur == nullptr ||666compactPtrGroupBase(Array[0]) != Cur->CompactPtrGroupBase) {667Cur = CreateGroup(compactPtrGroupBase(Array[0]));668if (Prev == nullptr)669Sci->FreeListInfo.BlockList.push_front(Cur);670else671Sci->FreeListInfo.BlockList.insert(Prev, Cur);672}673674// All the blocks are from the same group, just push without checking group675// id.676if (SameGroup) {677for (u32 I = 0; I < Size; ++I)678DCHECK_EQ(compactPtrGroupBase(Array[I]), Cur->CompactPtrGroupBase);679680InsertBlocks(Cur, Array, Size);681return;682}683684// The blocks are sorted by group id. Determine the segment of group and685// push them to their group together.686u32 Count = 1;687for (u32 I = 1; I < Size; ++I) {688if (compactPtrGroupBase(Array[I - 1]) != compactPtrGroupBase(Array[I])) {689DCHECK_EQ(compactPtrGroupBase(Array[I - 1]), Cur->CompactPtrGroupBase);690InsertBlocks(Cur, Array + I - Count, Count);691692while (Cur != nullptr &&693compactPtrGroupBase(Array[I]) > Cur->CompactPtrGroupBase) {694Prev = Cur;695Cur = Cur->Next;696}697698if (Cur == nullptr ||699compactPtrGroupBase(Array[I]) != Cur->CompactPtrGroupBase) {700Cur = CreateGroup(compactPtrGroupBase(Array[I]));701DCHECK_NE(Prev, nullptr);702Sci->FreeListInfo.BlockList.insert(Prev, Cur);703}704705Count = 1;706} else {707++Count;708}709}710711InsertBlocks(Cur, Array + Size - Count, Count);712}713714u16 popBlocksImpl(CacheT *C, uptr ClassId, SizeClassInfo *Sci,715CompactPtrT *ToArray, const u16 MaxBlockCount)716REQUIRES(Sci->Mutex) {717if (Sci->FreeListInfo.BlockList.empty())718return 0U;719720SinglyLinkedList<TransferBatchT> &Batches =721Sci->FreeListInfo.BlockList.front()->Batches;722723if (Batches.empty()) {724DCHECK_EQ(ClassId, SizeClassMap::BatchClassId);725BatchGroupT *BG = Sci->FreeListInfo.BlockList.front();726Sci->FreeListInfo.BlockList.pop_front();727728// Block used by `BatchGroup` is from BatchClassId. Turn the block into729// `TransferBatch` with single block.730TransferBatchT *TB = reinterpret_cast<TransferBatchT *>(BG);731ToArray[0] =732compactPtr(SizeClassMap::BatchClassId, reinterpret_cast<uptr>(TB));733Sci->FreeListInfo.PoppedBlocks += 1;734return 1U;735}736737// So far, instead of always filling the blocks to `MaxBlockCount`, we only738// examine single `TransferBatch` to minimize the time spent on the primary739// allocator. Besides, the sizes of `TransferBatch` and740// `CacheT::getMaxCached()` may also impact the time spent on accessing the741// primary allocator.742// TODO(chiahungduan): Evaluate if we want to always prepare `MaxBlockCount`743// blocks and/or adjust the size of `TransferBatch` according to744// `CacheT::getMaxCached()`.745TransferBatchT *B = Batches.front();746DCHECK_NE(B, nullptr);747DCHECK_GT(B->getCount(), 0U);748749// BachClassId should always take all blocks in the TransferBatch. Read the750// comment in `pushBatchClassBlocks()` for more details.751const u16 PopCount = ClassId == SizeClassMap::BatchClassId752? B->getCount()753: Min(MaxBlockCount, B->getCount());754B->moveNToArray(ToArray, PopCount);755756// TODO(chiahungduan): The deallocation of unused BatchClassId blocks can be757// done without holding `Mutex`.758if (B->empty()) {759Batches.pop_front();760// `TransferBatch` of BatchClassId is self-contained, no need to761// deallocate. Read the comment in `pushBatchClassBlocks()` for more762// details.763if (ClassId != SizeClassMap::BatchClassId)764C->deallocate(SizeClassMap::BatchClassId, B);765766if (Batches.empty()) {767BatchGroupT *BG = Sci->FreeListInfo.BlockList.front();768Sci->FreeListInfo.BlockList.pop_front();769770// We don't keep BatchGroup with zero blocks to avoid empty-checking771// while allocating. Note that block used for constructing BatchGroup is772// recorded as free blocks in the last element of BatchGroup::Batches.773// Which means, once we pop the last TransferBatch, the block is774// implicitly deallocated.775if (ClassId != SizeClassMap::BatchClassId)776C->deallocate(SizeClassMap::BatchClassId, BG);777}778}779780Sci->FreeListInfo.PoppedBlocks += PopCount;781return PopCount;782}783784NOINLINE bool populateFreeList(CacheT *C, uptr ClassId, SizeClassInfo *Sci)785REQUIRES(Sci->Mutex) {786uptr Region;787uptr Offset;788// If the size-class currently has a region associated to it, use it. The789// newly created blocks will be located after the currently allocated memory790// for that region (up to RegionSize). Otherwise, create a new region, where791// the new blocks will be carved from the beginning.792if (Sci->CurrentRegion) {793Region = Sci->CurrentRegion;794DCHECK_GT(Sci->CurrentRegionAllocated, 0U);795Offset = Sci->CurrentRegionAllocated;796} else {797DCHECK_EQ(Sci->CurrentRegionAllocated, 0U);798Region = allocateRegion(Sci, ClassId);799if (UNLIKELY(!Region))800return false;801C->getStats().add(StatMapped, RegionSize);802Sci->CurrentRegion = Region;803Offset = 0;804}805806const uptr Size = getSizeByClassId(ClassId);807const u16 MaxCount = CacheT::getMaxCached(Size);808DCHECK_GT(MaxCount, 0U);809// The maximum number of blocks we should carve in the region is dictated810// by the maximum number of batches we want to fill, and the amount of811// memory left in the current region (we use the lowest of the two). This812// will not be 0 as we ensure that a region can at least hold one block (via813// static_assert and at the end of this function).814const u32 NumberOfBlocks =815Min(MaxNumBatches * MaxCount,816static_cast<u32>((RegionSize - Offset) / Size));817DCHECK_GT(NumberOfBlocks, 0U);818819constexpr u32 ShuffleArraySize =820MaxNumBatches * TransferBatchT::MaxNumCached;821// Fill the transfer batches and put them in the size-class freelist. We822// need to randomize the blocks for security purposes, so we first fill a823// local array that we then shuffle before populating the batches.824CompactPtrT ShuffleArray[ShuffleArraySize];825DCHECK_LE(NumberOfBlocks, ShuffleArraySize);826827uptr P = Region + Offset;828for (u32 I = 0; I < NumberOfBlocks; I++, P += Size)829ShuffleArray[I] = reinterpret_cast<CompactPtrT>(P);830831if (ClassId != SizeClassMap::BatchClassId) {832u32 N = 1;833uptr CurGroup = compactPtrGroupBase(ShuffleArray[0]);834for (u32 I = 1; I < NumberOfBlocks; I++) {835if (UNLIKELY(compactPtrGroupBase(ShuffleArray[I]) != CurGroup)) {836shuffle(ShuffleArray + I - N, N, &Sci->RandState);837pushBlocksImpl(C, ClassId, Sci, ShuffleArray + I - N, N,838/*SameGroup=*/true);839N = 1;840CurGroup = compactPtrGroupBase(ShuffleArray[I]);841} else {842++N;843}844}845846shuffle(ShuffleArray + NumberOfBlocks - N, N, &Sci->RandState);847pushBlocksImpl(C, ClassId, Sci, &ShuffleArray[NumberOfBlocks - N], N,848/*SameGroup=*/true);849} else {850pushBatchClassBlocks(Sci, ShuffleArray, NumberOfBlocks);851}852853// Note that `PushedBlocks` and `PoppedBlocks` are supposed to only record854// the requests from `PushBlocks` and `PopBatch` which are external855// interfaces. `populateFreeList` is the internal interface so we should set856// the values back to avoid incorrectly setting the stats.857Sci->FreeListInfo.PushedBlocks -= NumberOfBlocks;858859const uptr AllocatedUser = Size * NumberOfBlocks;860C->getStats().add(StatFree, AllocatedUser);861DCHECK_LE(Sci->CurrentRegionAllocated + AllocatedUser, RegionSize);862// If there is not enough room in the region currently associated to fit863// more blocks, we deassociate the region by resetting CurrentRegion and864// CurrentRegionAllocated. Otherwise, update the allocated amount.865if (RegionSize - (Sci->CurrentRegionAllocated + AllocatedUser) < Size) {866Sci->CurrentRegion = 0;867Sci->CurrentRegionAllocated = 0;868} else {869Sci->CurrentRegionAllocated += AllocatedUser;870}871Sci->AllocatedUser += AllocatedUser;872873return true;874}875876void getStats(ScopedString *Str, uptr ClassId, SizeClassInfo *Sci)877REQUIRES(Sci->Mutex) {878if (Sci->AllocatedUser == 0)879return;880const uptr BlockSize = getSizeByClassId(ClassId);881const uptr InUse =882Sci->FreeListInfo.PoppedBlocks - Sci->FreeListInfo.PushedBlocks;883const uptr BytesInFreeList = Sci->AllocatedUser - InUse * BlockSize;884uptr PushedBytesDelta = 0;885if (BytesInFreeList >= Sci->ReleaseInfo.BytesInFreeListAtLastCheckpoint) {886PushedBytesDelta =887BytesInFreeList - Sci->ReleaseInfo.BytesInFreeListAtLastCheckpoint;888}889const uptr AvailableChunks = Sci->AllocatedUser / BlockSize;890Str->append(" %02zu (%6zu): mapped: %6zuK popped: %7zu pushed: %7zu "891"inuse: %6zu avail: %6zu releases: %6zu last released: %6zuK "892"latest pushed bytes: %6zuK\n",893ClassId, getSizeByClassId(ClassId), Sci->AllocatedUser >> 10,894Sci->FreeListInfo.PoppedBlocks, Sci->FreeListInfo.PushedBlocks,895InUse, AvailableChunks, Sci->ReleaseInfo.RangesReleased,896Sci->ReleaseInfo.LastReleasedBytes >> 10,897PushedBytesDelta >> 10);898}899900void getSizeClassFragmentationInfo(SizeClassInfo *Sci, uptr ClassId,901ScopedString *Str) REQUIRES(Sci->Mutex) {902const uptr BlockSize = getSizeByClassId(ClassId);903const uptr First = Sci->MinRegionIndex;904const uptr Last = Sci->MaxRegionIndex;905const uptr Base = First * RegionSize;906const uptr NumberOfRegions = Last - First + 1U;907auto SkipRegion = [this, First, ClassId](uptr RegionIndex) {908ScopedLock L(ByteMapMutex);909return (PossibleRegions[First + RegionIndex] - 1U) != ClassId;910};911912FragmentationRecorder Recorder;913if (!Sci->FreeListInfo.BlockList.empty()) {914PageReleaseContext Context =915markFreeBlocks(Sci, ClassId, BlockSize, Base, NumberOfRegions,916ReleaseToOS::ForceAll);917releaseFreeMemoryToOS(Context, Recorder, SkipRegion);918}919920const uptr PageSize = getPageSizeCached();921const uptr TotalBlocks = Sci->AllocatedUser / BlockSize;922const uptr InUseBlocks =923Sci->FreeListInfo.PoppedBlocks - Sci->FreeListInfo.PushedBlocks;924uptr AllocatedPagesCount = 0;925if (TotalBlocks != 0U) {926for (uptr I = 0; I < NumberOfRegions; ++I) {927if (SkipRegion(I))928continue;929AllocatedPagesCount += RegionSize / PageSize;930}931932DCHECK_NE(AllocatedPagesCount, 0U);933}934935DCHECK_GE(AllocatedPagesCount, Recorder.getReleasedPagesCount());936const uptr InUsePages =937AllocatedPagesCount - Recorder.getReleasedPagesCount();938const uptr InUseBytes = InUsePages * PageSize;939940uptr Integral;941uptr Fractional;942computePercentage(BlockSize * InUseBlocks, InUsePages * PageSize, &Integral,943&Fractional);944Str->append(" %02zu (%6zu): inuse/total blocks: %6zu/%6zu inuse/total "945"pages: %6zu/%6zu inuse bytes: %6zuK util: %3zu.%02zu%%\n",946ClassId, BlockSize, InUseBlocks, TotalBlocks, InUsePages,947AllocatedPagesCount, InUseBytes >> 10, Integral, Fractional);948}949950NOINLINE uptr releaseToOSMaybe(SizeClassInfo *Sci, uptr ClassId,951ReleaseToOS ReleaseType = ReleaseToOS::Normal)952REQUIRES(Sci->Mutex) {953const uptr BlockSize = getSizeByClassId(ClassId);954955DCHECK_GE(Sci->FreeListInfo.PoppedBlocks, Sci->FreeListInfo.PushedBlocks);956const uptr BytesInFreeList =957Sci->AllocatedUser -958(Sci->FreeListInfo.PoppedBlocks - Sci->FreeListInfo.PushedBlocks) *959BlockSize;960961if (UNLIKELY(BytesInFreeList == 0))962return 0;963964// ====================================================================== //965// 1. Check if we have enough free blocks and if it's worth doing a page966// release.967// ====================================================================== //968if (ReleaseType != ReleaseToOS::ForceAll &&969!hasChanceToReleasePages(Sci, BlockSize, BytesInFreeList,970ReleaseType)) {971return 0;972}973974const uptr First = Sci->MinRegionIndex;975const uptr Last = Sci->MaxRegionIndex;976DCHECK_NE(Last, 0U);977DCHECK_LE(First, Last);978uptr TotalReleasedBytes = 0;979const uptr Base = First * RegionSize;980const uptr NumberOfRegions = Last - First + 1U;981982// ==================================================================== //983// 2. Mark the free blocks and we can tell which pages are in-use by984// querying `PageReleaseContext`.985// ==================================================================== //986PageReleaseContext Context = markFreeBlocks(Sci, ClassId, BlockSize, Base,987NumberOfRegions, ReleaseType);988if (!Context.hasBlockMarked())989return 0;990991// ==================================================================== //992// 3. Release the unused physical pages back to the OS.993// ==================================================================== //994ReleaseRecorder Recorder(Base);995auto SkipRegion = [this, First, ClassId](uptr RegionIndex) {996ScopedLock L(ByteMapMutex);997return (PossibleRegions[First + RegionIndex] - 1U) != ClassId;998};999releaseFreeMemoryToOS(Context, Recorder, SkipRegion);10001001if (Recorder.getReleasedRangesCount() > 0) {1002Sci->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;1003Sci->ReleaseInfo.RangesReleased += Recorder.getReleasedRangesCount();1004Sci->ReleaseInfo.LastReleasedBytes = Recorder.getReleasedBytes();1005TotalReleasedBytes += Sci->ReleaseInfo.LastReleasedBytes;1006}1007Sci->ReleaseInfo.LastReleaseAtNs = getMonotonicTimeFast();10081009return TotalReleasedBytes;1010}10111012bool hasChanceToReleasePages(SizeClassInfo *Sci, uptr BlockSize,1013uptr BytesInFreeList, ReleaseToOS ReleaseType)1014REQUIRES(Sci->Mutex) {1015DCHECK_GE(Sci->FreeListInfo.PoppedBlocks, Sci->FreeListInfo.PushedBlocks);1016const uptr PageSize = getPageSizeCached();10171018if (BytesInFreeList <= Sci->ReleaseInfo.BytesInFreeListAtLastCheckpoint)1019Sci->ReleaseInfo.BytesInFreeListAtLastCheckpoint = BytesInFreeList;10201021// Always update `BytesInFreeListAtLastCheckpoint` with the smallest value1022// so that we won't underestimate the releasable pages. For example, the1023// following is the region usage,1024//1025// BytesInFreeListAtLastCheckpoint AllocatedUser1026// v v1027// |--------------------------------------->1028// ^ ^1029// BytesInFreeList ReleaseThreshold1030//1031// In general, if we have collected enough bytes and the amount of free1032// bytes meets the ReleaseThreshold, we will try to do page release. If we1033// don't update `BytesInFreeListAtLastCheckpoint` when the current1034// `BytesInFreeList` is smaller, we may take longer time to wait for enough1035// freed blocks because we miss the bytes between1036// (BytesInFreeListAtLastCheckpoint - BytesInFreeList).1037const uptr PushedBytesDelta =1038BytesInFreeList - Sci->ReleaseInfo.BytesInFreeListAtLastCheckpoint;1039if (PushedBytesDelta < PageSize)1040return false;10411042// Releasing smaller blocks is expensive, so we want to make sure that a1043// significant amount of bytes are free, and that there has been a good1044// amount of batches pushed to the freelist before attempting to release.1045if (isSmallBlock(BlockSize) && ReleaseType == ReleaseToOS::Normal)1046if (PushedBytesDelta < Sci->AllocatedUser / 16U)1047return false;10481049if (ReleaseType == ReleaseToOS::Normal) {1050const s32 IntervalMs = atomic_load_relaxed(&ReleaseToOsIntervalMs);1051if (IntervalMs < 0)1052return false;10531054// The constant 8 here is selected from profiling some apps and the number1055// of unreleased pages in the large size classes is around 16 pages or1056// more. Choose half of it as a heuristic and which also avoids page1057// release every time for every pushBlocks() attempt by large blocks.1058const bool ByPassReleaseInterval =1059isLargeBlock(BlockSize) && PushedBytesDelta > 8 * PageSize;1060if (!ByPassReleaseInterval) {1061if (Sci->ReleaseInfo.LastReleaseAtNs +1062static_cast<u64>(IntervalMs) * 1000000 >1063getMonotonicTimeFast()) {1064// Memory was returned recently.1065return false;1066}1067}1068} // if (ReleaseType == ReleaseToOS::Normal)10691070return true;1071}10721073PageReleaseContext markFreeBlocks(SizeClassInfo *Sci, const uptr ClassId,1074const uptr BlockSize, const uptr Base,1075const uptr NumberOfRegions,1076ReleaseToOS ReleaseType)1077REQUIRES(Sci->Mutex) {1078const uptr PageSize = getPageSizeCached();1079const uptr GroupSize = (1UL << GroupSizeLog);1080const uptr CurGroupBase =1081compactPtrGroupBase(compactPtr(ClassId, Sci->CurrentRegion));10821083PageReleaseContext Context(BlockSize, NumberOfRegions,1084/*ReleaseSize=*/RegionSize);10851086auto DecompactPtr = [](CompactPtrT CompactPtr) {1087return reinterpret_cast<uptr>(CompactPtr);1088};1089for (BatchGroupT &BG : Sci->FreeListInfo.BlockList) {1090const uptr GroupBase = decompactGroupBase(BG.CompactPtrGroupBase);1091// The `GroupSize` may not be divided by `BlockSize`, which means there is1092// an unused space at the end of Region. Exclude that space to avoid1093// unused page map entry.1094uptr AllocatedGroupSize = GroupBase == CurGroupBase1095? Sci->CurrentRegionAllocated1096: roundDownSlow(GroupSize, BlockSize);1097if (AllocatedGroupSize == 0)1098continue;10991100// TransferBatches are pushed in front of BG.Batches. The first one may1101// not have all caches used.1102const uptr NumBlocks = (BG.Batches.size() - 1) * BG.MaxCachedPerBatch +1103BG.Batches.front()->getCount();1104const uptr BytesInBG = NumBlocks * BlockSize;11051106if (ReleaseType != ReleaseToOS::ForceAll) {1107if (BytesInBG <= BG.BytesInBGAtLastCheckpoint) {1108BG.BytesInBGAtLastCheckpoint = BytesInBG;1109continue;1110}11111112const uptr PushedBytesDelta = BytesInBG - BG.BytesInBGAtLastCheckpoint;1113if (PushedBytesDelta < PageSize)1114continue;11151116// Given the randomness property, we try to release the pages only if1117// the bytes used by free blocks exceed certain proportion of allocated1118// spaces.1119if (isSmallBlock(BlockSize) && (BytesInBG * 100U) / AllocatedGroupSize <1120(100U - 1U - BlockSize / 16U)) {1121continue;1122}1123}11241125// TODO: Consider updating this after page release if `ReleaseRecorder`1126// can tell the released bytes in each group.1127BG.BytesInBGAtLastCheckpoint = BytesInBG;11281129const uptr MaxContainedBlocks = AllocatedGroupSize / BlockSize;1130const uptr RegionIndex = (GroupBase - Base) / RegionSize;11311132if (NumBlocks == MaxContainedBlocks) {1133for (const auto &It : BG.Batches)1134for (u16 I = 0; I < It.getCount(); ++I)1135DCHECK_EQ(compactPtrGroupBase(It.get(I)), BG.CompactPtrGroupBase);11361137const uptr To = GroupBase + AllocatedGroupSize;1138Context.markRangeAsAllCounted(GroupBase, To, GroupBase, RegionIndex,1139AllocatedGroupSize);1140} else {1141DCHECK_LT(NumBlocks, MaxContainedBlocks);11421143// Note that we don't always visit blocks in each BatchGroup so that we1144// may miss the chance of releasing certain pages that cross1145// BatchGroups.1146Context.markFreeBlocksInRegion(BG.Batches, DecompactPtr, GroupBase,1147RegionIndex, AllocatedGroupSize,1148/*MayContainLastBlockInRegion=*/true);1149}11501151// We may not be able to do the page release In a rare case that we may1152// fail on PageMap allocation.1153if (UNLIKELY(!Context.hasBlockMarked()))1154break;1155}11561157return Context;1158}11591160SizeClassInfo SizeClassInfoArray[NumClasses] = {};11611162HybridMutex ByteMapMutex;1163// Track the regions in use, 0 is unused, otherwise store ClassId + 1.1164ByteMap PossibleRegions GUARDED_BY(ByteMapMutex) = {};1165atomic_s32 ReleaseToOsIntervalMs = {};1166// Unless several threads request regions simultaneously from different size1167// classes, the stash rarely contains more than 1 entry.1168static constexpr uptr MaxStashedRegions = 4;1169HybridMutex RegionsStashMutex;1170uptr NumberOfStashedRegions GUARDED_BY(RegionsStashMutex) = 0;1171uptr RegionsStash[MaxStashedRegions] GUARDED_BY(RegionsStashMutex) = {};1172};11731174} // namespace scudo11751176#endif // SCUDO_PRIMARY32_H_117711781179