/* * $Id: MemPool.cc,v 1.10.2.1 2008/02/25 03:41:39 amosjeffries Exp $ * * DEBUG: section 63 Low Level Memory Pool Management * AUTHOR: Alex Rousskov, Andres Kroonmaa, Robert Collins * * SQUID Internet Object Cache http://squid.nlanr.net/Squid/ * ---------------------------------------------------------- * * Squid is the result of efforts by numerous individuals from the * Internet community. Development is led by Duane Wessels of the * National Laboratory for Applied Network Research and funded by the * National Science Foundation. Squid is Copyrighted (C) 1998 by * the Regents of the University of California. Please see the * COPYRIGHT file for full details. Squid incorporates software * developed and/or copyrighted by other sources. Please see the * CREDITS file for full details. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111, USA. * */ /* * Old way: * xmalloc each item separately, upon free stack into idle pool array. * each item is individually malloc()ed from system, imposing libmalloc * overhead, and additionally we add our overhead of pointer size per item * as we keep a list of pointer to free items. * * Chunking: * xmalloc Chunk that fits at least MEM_MIN_FREE (32) items in an array, but * limit Chunk size to MEM_CHUNK_MAX_SIZE (256K). Chunk size is rounded up to * MEM_PAGE_SIZE (4K), trying to have chunks in multiples of VM_PAGE size. * Minimum Chunk size is MEM_CHUNK_SIZE (16K). * A number of items fits into a single chunk, depending on item size. * Maximum number of items per chunk is limited to MEM_MAX_FREE (65535). * * We populate Chunk with a linkedlist, each node at first word of item, * and pointing at next free item. Chunk->FreeList is pointing at first * free node. Thus we stuff free housekeeping into the Chunk itself, and * omit pointer overhead per item. * * Chunks are created on demand, and new chunks are inserted into linklist * of chunks so that Chunks with smaller pointer value are placed closer * to the linklist head. Head is a hotspot, servicing most of requests, so * slow sorting occurs and Chunks in highest memory tend to become idle * and freeable. * * event is registered that runs every 15 secs and checks reference time * of each idle chunk. If a chunk is not referenced for 15 secs, it is * released. * * [If mem_idle_limit is exceeded with pools, every chunk that becomes * idle is immediately considered for release, unless this is the only * chunk with free items in it.] (not implemented) * * In cachemgr output, there are new columns for chunking. Special item, * Frag, is shown to estimate approximately fragmentation of chunked * pools. Fragmentation is calculated by taking amount of items in use, * calculating needed amount of chunks to fit all, and then comparing to * actual amount of chunks in use. Frag number, in percent, is showing * how many percent of chunks are in use excessively. 100% meaning that * twice the needed amount of chunks are in use. * "part" item shows number of chunks partially filled. This shows how * badly fragmentation is spread across all chunks. * * Andres Kroonmaa. * Copyright (c) 2003, Robert Collins */ #include "config.h" #if HAVE_ASSERT_H #include #endif #include "MemPool.h" #define FLUSH_LIMIT 1000 /* Flush memPool counters to memMeters after flush limit calls */ #define MEM_MAX_MMAP_CHUNKS 2048 #if HAVE_STRING_H #include #endif /* * XXX This is a boundary violation between lib and src.. would be good * if it could be solved otherwise, but left for now. */ extern time_t squid_curtime; /* local data */ static MemPoolMeter TheMeter; static MemPoolIterator Iterator; static int Pool_id_counter = 0; /* local prototypes */ static int memCompChunks(MemChunk * const &, MemChunk * const &); static int memCompObjChunks(void * const &, MemChunk * const &); MemPools & MemPools::GetInstance() { /* Must use this idiom, as we can be double-initialised * if we are called during static initialisations. */ if (!Instance) Instance = new MemPools; return *Instance; } MemPools * MemPools::Instance = NULL; MemPoolIterator * memPoolIterate(void) { Iterator.pool = MemPools::GetInstance().pools; return &Iterator; } void memPoolIterateDone(MemPoolIterator ** iter) { assert(iter != NULL); Iterator.pool = NULL; *iter = NULL; } MemImplementingAllocator * memPoolIterateNext(MemPoolIterator * iter) { MemImplementingAllocator *pool; assert(iter != NULL); pool = iter->pool; if (!pool) return NULL; iter->pool = pool->next; return pool; } void MemPools::setIdleLimit(size_t new_idle_limit) { mem_idle_limit = new_idle_limit; } size_t MemPools::idleLimit() const { return mem_idle_limit; } /* Compare chunks */ static int memCompChunks(MemChunk * const &chunkA, MemChunk * const &chunkB) { if (chunkA->objCache > chunkB->objCache) return 1; else if (chunkA->objCache < chunkB->objCache) return -1; else return 0; } /* Compare object to chunk */ static int memCompObjChunks(void *const &obj, MemChunk * const &chunk) { /* object is lower in memory than the chunks arena */ if (obj < chunk->objCache) return -1; /* object is within the pool */ if (obj < (void *) ((char *) chunk->objCache + chunk->pool->chunk_size)) return 0; /* object is above the pool */ return 1; } MemChunk::MemChunk(MemPool *aPool) { /* should have a pool for this too - * note that this requres: * allocate one chunk for the pool of chunks's first chunk * allocate a chunk from that pool * move the contents of one chunk into the other * free the first chunk. */ inuse_count = 0; next = NULL; pool = aPool; objCache = xcalloc(1, pool->chunk_size); freeList = objCache; void **Free = (void **)freeList; for (int i = 1; i < pool->chunk_capacity; i++) { *Free = (void *) ((char *) Free + pool->obj_size); void **nextFree = (void **)*Free; (void) VALGRIND_MAKE_MEM_NOACCESS(Free, pool->obj_size); Free = nextFree; } nextFreeChunk = pool->nextFreeChunk; pool->nextFreeChunk = this; memMeterAdd(pool->getMeter().alloc, pool->chunk_capacity); memMeterAdd(pool->getMeter().idle, pool->chunk_capacity); pool->idle += pool->chunk_capacity; pool->chunkCount++; lastref = squid_curtime; pool->allChunks.insert(this, memCompChunks); } MemPool::MemPool(const char *aLabel, size_t aSize) : MemImplementingAllocator(aLabel, aSize) { chunk_size = 0; chunk_capacity = 0; memPID = 0; chunkCount = 0; inuse = 0; idle = 0; freeCache = 0; nextFreeChunk = 0; Chunks = 0; next = 0; MemImplementingAllocator *last_pool; assert(aLabel != NULL && aSize); setChunkSize(MEM_CHUNK_SIZE); /* Append as Last */ for (last_pool = MemPools::GetInstance().pools; last_pool && last_pool->next;) last_pool = last_pool->next; if (last_pool) last_pool->next = this; else MemPools::GetInstance().pools = this; memPID = ++Pool_id_counter; } MemChunk::~MemChunk() { memMeterDel(pool->getMeter().alloc, pool->chunk_capacity); memMeterDel(pool->getMeter().idle, pool->chunk_capacity); pool->idle -= pool->chunk_capacity; pool->chunkCount--; pool->allChunks.remove(this, memCompChunks); xfree(objCache); } void MemPool::push(void *obj) { void **Free; /* XXX We should figure out a sane way of avoiding having to clear * all buffers. For example data buffers such as used by MemBuf do * not really need to be cleared.. There was a condition based on * the object size here, but such condition is not safe. */ memset(obj, 0, obj_size); Free = (void **)obj; *Free = freeCache; freeCache = obj; (void) VALGRIND_MAKE_MEM_NOACCESS(obj, obj_size); } /* * Find a chunk with a free item. * Create new chunk on demand if no chunk with frees found. * Insert new chunk in front of lowest ram chunk, making it preferred in future, * and resulting slow compaction towards lowest ram area. */ void * MemPool::get() { void **Free; /* first, try cache */ if (freeCache) { Free = (void **)freeCache; (void) VALGRIND_MAKE_MEM_DEFINED(Free, obj_size); freeCache = *Free; *Free = NULL; return Free; } /* then try perchunk freelist chain */ if (nextFreeChunk == NULL) { /* no chunk with frees, so create new one */ createChunk(); } /* now we have some in perchunk freelist chain */ MemChunk *chunk = nextFreeChunk; Free = (void **)chunk->freeList; chunk->freeList = *Free; *Free = NULL; chunk->inuse_count++; chunk->lastref = squid_curtime; if (chunk->freeList == NULL) { /* last free in this chunk, so remove us from perchunk freelist chain */ nextFreeChunk = chunk->nextFreeChunk; } (void) VALGRIND_MAKE_MEM_DEFINED(Free, obj_size); return Free; } /* just create a new chunk and place it into a good spot in the chunk chain */ void MemPool::createChunk() { MemChunk *chunk, *newChunk; newChunk = new MemChunk(this); chunk = Chunks; if (chunk == NULL) { /* first chunk in pool */ Chunks = newChunk; return; } if (newChunk->objCache < chunk->objCache) { /* we are lowest ram chunk, insert as first chunk */ newChunk->next = chunk; Chunks = newChunk; return; } while (chunk->next) { if (newChunk->objCache < chunk->next->objCache) { /* new chunk is in lower ram, insert here */ newChunk->next = chunk->next; chunk->next = newChunk; return; } chunk = chunk->next; } /* we are the worst chunk in chain, add as last */ chunk->next = newChunk; } /* Change the default calue of defaultIsChunked to override * all pools - including those used before main() starts where * MemPools::GetInstance().setDefaultPoolChunking() can be called. */ MemPools::MemPools() : pools(NULL), mem_idle_limit(2 * MB), poolCount (0), defaultIsChunked (!DISABLE_POOLS && !RUNNING_ON_VALGRIND) { char *cfg = getenv("MEMPOOLS"); if (cfg) defaultIsChunked = atoi(cfg); #if HAVE_MALLOPT && M_MMAP_MAX mallopt(M_MMAP_MAX, MEM_MAX_MMAP_CHUNKS); #endif } void MemPool::setChunkSize(size_t chunksize) { int cap; size_t csize = chunksize; if (Chunks) /* unsafe to tamper */ return; csize = ((csize + MEM_PAGE_SIZE - 1) / MEM_PAGE_SIZE) * MEM_PAGE_SIZE; /* round up to page size */ cap = csize / obj_size; if (cap < MEM_MIN_FREE) cap = MEM_MIN_FREE; if (cap * obj_size > MEM_CHUNK_MAX_SIZE) cap = MEM_CHUNK_MAX_SIZE / obj_size; if (cap > MEM_MAX_FREE) cap = MEM_MAX_FREE; if (cap < 1) cap = 1; csize = cap * obj_size; csize = ((csize + MEM_PAGE_SIZE - 1) / MEM_PAGE_SIZE) * MEM_PAGE_SIZE; /* round up to page size */ cap = csize / obj_size; chunk_capacity = cap; chunk_size = csize; } MemImplementingAllocator * MemPools::create(const char *label, size_t obj_size) { return create (label, obj_size, defaultIsChunked); } MemImplementingAllocator * MemPools::create(const char *label, size_t obj_size, bool const chunked) { ++poolCount; if (chunked) return new MemPool (label, obj_size); else return new MemMalloc (label, obj_size); } void MemPools::setDefaultPoolChunking(bool const &aBool) { defaultIsChunked = aBool; } /* * warning: we do not clean this entry from Pools assuming destruction * is used at the end of the program only */ MemPool::~MemPool() { MemChunk *chunk, *fchunk; MemImplementingAllocator *find_pool, *prev_pool; flushMetersFull(); clean(0); assert(inuse == 0 && "While trying to destroy pool"); chunk = Chunks; while( (fchunk = chunk) != NULL) { chunk = chunk->next; delete fchunk; } /* TODO we should be doing something about the original Chunks pointer here. */ assert(MemPools::GetInstance().pools != NULL && "Called MemPool::~MemPool, but no pool exists!"); /* Pool clean, remove it from List and free */ for (find_pool = MemPools::GetInstance().pools, prev_pool = NULL; (find_pool && this != find_pool); find_pool = find_pool->next) prev_pool = find_pool; assert(find_pool != NULL && "pool to destroy not found"); if (prev_pool) prev_pool->next = next; else MemPools::GetInstance().pools = next; --MemPools::GetInstance().poolCount; } char const * MemAllocator::objectType() const { return label; } int MemAllocator::inUseCount() { return getInUseCount(); } void MemImplementingAllocator::flushMeters() { size_t calls; calls = free_calls; if (calls) { getMeter().gb_freed.count += calls; memMeterDel(getMeter().inuse, calls); memMeterAdd(getMeter().idle, calls); free_calls = 0; } calls = alloc_calls; if (calls) { meter.gb_saved.count += calls; memMeterAdd(meter.inuse, calls); memMeterDel(meter.idle, calls); alloc_calls = 0; } } void MemImplementingAllocator::flushMetersFull() { flushMeters(); getMeter().gb_saved.bytes = getMeter().gb_saved.count * obj_size; getMeter().gb_freed.bytes = getMeter().gb_freed.count * obj_size; } void MemPoolMeter::flush() { alloc.level = 0; inuse.level = 0; idle.level = 0; gb_saved.count = 0; gb_saved.bytes = 0; gb_freed.count = 0; gb_freed.bytes = 0; } /* * Updates all pool counters, and recreates TheMeter totals from all pools */ void MemPools::flushMeters() { MemImplementingAllocator *pool; MemPoolIterator *iter; TheMeter.flush(); iter = memPoolIterate(); while ((pool = memPoolIterateNext(iter))) { pool->flushMetersFull(); memMeterAdd(TheMeter.alloc, pool->getMeter().alloc.level * pool->obj_size); memMeterAdd(TheMeter.inuse, pool->getMeter().inuse.level * pool->obj_size); memMeterAdd(TheMeter.idle, pool->getMeter().idle.level * pool->obj_size); TheMeter.gb_saved.count += pool->getMeter().gb_saved.count; TheMeter.gb_freed.count += pool->getMeter().gb_freed.count; TheMeter.gb_saved.bytes += pool->getMeter().gb_saved.bytes; TheMeter.gb_freed.bytes += pool->getMeter().gb_freed.bytes; } memPoolIterateDone(&iter); } void * MemMalloc::allocate() { inuse++; return xcalloc(1, obj_size); } void MemMalloc::deallocate(void *obj) { inuse--; xfree(obj); } void * MemImplementingAllocator::alloc() { if (++alloc_calls == FLUSH_LIMIT) flushMeters(); return allocate(); } void MemImplementingAllocator::free(void *obj) { assert(obj != NULL); (void) VALGRIND_CHECK_MEM_IS_ADDRESSABLE(obj, obj_size); deallocate(obj); ++free_calls; } int MemPool::getInUseCount() { return inuse; } void * MemPool::allocate() { void *p = get(); assert(idle); --idle; ++inuse; return p; } void MemPool::deallocate(void *obj) { push(obj); assert(inuse); --inuse; ++idle; } void MemPool::convertFreeCacheToChunkFreeCache() { void *Free; /* * OK, so we have to go through all the global freeCache and find the Chunk * any given Free belongs to, and stuff it into that Chunk's freelist */ while ((Free = freeCache) != NULL) { MemChunk *chunk = NULL; chunk = const_cast(*allChunks.find(Free, memCompObjChunks)); assert(splayLastResult == 0); assert(chunk->inuse_count > 0); chunk->inuse_count--; (void) VALGRIND_MAKE_MEM_DEFINED(Free, sizeof(void *)); freeCache = *(void **)Free; /* remove from global cache */ *(void **)Free = chunk->freeList; /* stuff into chunks freelist */ (void) VALGRIND_MAKE_MEM_NOACCESS(Free, sizeof(void *)); chunk->freeList = Free; chunk->lastref = squid_curtime; } } /* removes empty Chunks from pool */ void MemPool::clean(time_t maxage) { MemChunk *chunk, *freechunk, *listTail; time_t age; if (!this) return; if (!Chunks) return; flushMetersFull(); convertFreeCacheToChunkFreeCache(); /* Now we have all chunks in this pool cleared up, all free items returned to their home */ /* We start now checking all chunks to see if we can release any */ /* We start from Chunks->next, so first chunk is not released */ /* Recreate nextFreeChunk list from scratch */ chunk = Chunks; while ((freechunk = chunk->next) != NULL) { age = squid_curtime - freechunk->lastref; freechunk->nextFreeChunk = NULL; if (freechunk->inuse_count == 0) if (age >= maxage) { chunk->next = freechunk->next; delete freechunk; freechunk = NULL; } if (chunk->next == NULL) break; chunk = chunk->next; } /* Recreate nextFreeChunk list from scratch */ /* Populate nextFreeChunk list in order of "most filled chunk first" */ /* in case of equal fill, put chunk in lower ram first */ /* First (create time) chunk is always on top, no matter how full */ chunk = Chunks; nextFreeChunk = chunk; chunk->nextFreeChunk = NULL; while (chunk->next) { chunk->next->nextFreeChunk = NULL; if (chunk->next->inuse_count < chunk_capacity) { listTail = nextFreeChunk; while (listTail->nextFreeChunk) { if (chunk->next->inuse_count > listTail->nextFreeChunk->inuse_count) break; if ((chunk->next->inuse_count == listTail->nextFreeChunk->inuse_count) && (chunk->next->objCache < listTail->nextFreeChunk->objCache)) break; listTail = listTail->nextFreeChunk; } chunk->next->nextFreeChunk = listTail->nextFreeChunk; listTail->nextFreeChunk = chunk->next; } chunk = chunk->next; } /* We started from 2nd chunk. If first chunk is full, remove it */ if (nextFreeChunk->inuse_count == chunk_capacity) nextFreeChunk = nextFreeChunk->nextFreeChunk; return; } /* * Returns all cached frees to their home chunks * If chunks unreferenced age is over, destroys Idle chunk * Flushes meters for a pool * If pool is not specified, iterates through all pools. * When used for all pools, if new_idle_limit is above -1, new * idle memory limit is set before Cleanup. This allows to shrink * memPool memory usage to specified minimum. */ void MemPools::clean(time_t maxage) { MemImplementingAllocator *pool; MemPoolIterator *iter; int shift = 1; flushMeters(); if (TheMeter.idle.level > mem_idle_limit) maxage = shift = 0; iter = memPoolIterate(); while ((pool = memPoolIterateNext(iter))) if (pool->idleTrigger(shift)) pool->clean(maxage); memPoolIterateDone(&iter); } bool MemPool::idleTrigger(int shift) const { return getMeter().idle.level > (chunk_capacity << shift); } /* Persistent Pool stats. for GlobalStats accumulation */ static MemPoolStats pp_stats; /* * Update MemPoolStats struct for single pool */ int MemPool::getStats(MemPoolStats * stats) { MemChunk *chunk; int chunks_free = 0; int chunks_partial = 0; if (stats != &pp_stats) /* need skip memset for GlobalStats accumulation */ /* XXX Fixme */ memset(stats, 0, sizeof(MemPoolStats)); clean((time_t) 555555); /* don't want to get chunks released before reporting */ stats->pool = this; stats->label = objectType(); stats->meter = &getMeter(); stats->obj_size = obj_size; stats->chunk_capacity = chunk_capacity; /* gather stats for each Chunk */ chunk = Chunks; while (chunk) { if (chunk->inuse_count == 0) chunks_free++; else if (chunk->inuse_count < chunk_capacity) chunks_partial++; chunk = chunk->next; } stats->chunks_alloc += chunkCount; stats->chunks_inuse += chunkCount - chunks_free; stats->chunks_partial += chunks_partial; stats->chunks_free += chunks_free; stats->items_alloc += getMeter().alloc.level; stats->items_inuse += getMeter().inuse.level; stats->items_idle += getMeter().idle.level; stats->overhead += sizeof(MemPool) + chunkCount * sizeof(MemChunk) + strlen(objectType()) + 1; return getMeter().inuse.level; } /* TODO extract common logic to MemAllocate */ int MemMalloc::getStats(MemPoolStats * stats) { if (stats != &pp_stats) /* need skip memset for GlobalStats accumulation */ /* XXX Fixme */ memset(stats, 0, sizeof(MemPoolStats)); stats->pool = this; stats->label = objectType(); stats->meter = &getMeter(); stats->obj_size = obj_size; stats->chunk_capacity = 0; stats->chunks_alloc += 0; stats->chunks_inuse += 0; stats->chunks_partial += 0; stats->chunks_free += 0; stats->items_alloc += getMeter().alloc.level; stats->items_inuse += getMeter().inuse.level; stats->items_idle += getMeter().idle.level; stats->overhead += sizeof(MemMalloc) + strlen(objectType()) + 1; return getMeter().inuse.level; } int MemMalloc::getInUseCount() { return inuse; } /* * Totals statistics is returned */ int memPoolGetGlobalStats(MemPoolGlobalStats * stats) { int pools_inuse = 0; MemAllocator *pool; MemPoolIterator *iter; memset(stats, 0, sizeof(MemPoolGlobalStats)); memset(&pp_stats, 0, sizeof(MemPoolStats)); MemPools::GetInstance().flushMeters(); /* recreate TheMeter */ /* gather all stats for Totals */ iter = memPoolIterate(); while ((pool = memPoolIterateNext(iter))) { if (pool->getStats(&pp_stats) > 0) pools_inuse++; } memPoolIterateDone(&iter); stats->TheMeter = &TheMeter; stats->tot_pools_alloc = MemPools::GetInstance().poolCount; stats->tot_pools_inuse = pools_inuse; stats->tot_pools_mempid = Pool_id_counter; stats->tot_chunks_alloc = pp_stats.chunks_alloc; stats->tot_chunks_inuse = pp_stats.chunks_inuse; stats->tot_chunks_partial = pp_stats.chunks_partial; stats->tot_chunks_free = pp_stats.chunks_free; stats->tot_items_alloc = pp_stats.items_alloc; stats->tot_items_inuse = pp_stats.items_inuse; stats->tot_items_idle = pp_stats.items_idle; stats->tot_overhead += pp_stats.overhead + MemPools::GetInstance().poolCount * sizeof(MemPool *); stats->mem_idle_limit = MemPools::GetInstance().mem_idle_limit; return pools_inuse; } MemAllocator::MemAllocator(char const *aLabel) : label(aLabel) { } size_t MemAllocator::RoundedSize(size_t s) { return ((s + sizeof(void*) - 1) / sizeof(void*)) * sizeof(void*); } MemMalloc::MemMalloc(char const *label, size_t aSize) : MemImplementingAllocator(label, aSize) { inuse = 0; } bool MemMalloc::idleTrigger(int shift) const { return false; } void MemMalloc::clean(time_t maxage) { } int memPoolInUseCount(MemAllocator * pool) { return pool->inUseCount(); } int memPoolsTotalAllocated(void) { MemPoolGlobalStats stats; memPoolGetGlobalStats(&stats); return stats.TheMeter->alloc.level; } void * MemAllocatorProxy::alloc() { return getAllocator()->alloc(); } void MemAllocatorProxy::free(void *address) { getAllocator()->free(address); /* TODO: check for empty, and if so, if the default type has altered, * switch */ } MemAllocator * MemAllocatorProxy::getAllocator() const { if (!theAllocator) theAllocator = MemPools::GetInstance().create(objectType(), size); return theAllocator; } int MemAllocatorProxy::inUseCount() const { if (!theAllocator) return 0; else return memPoolInUseCount(theAllocator); } size_t MemAllocatorProxy::objectSize() const { return size; } char const * MemAllocatorProxy::objectType() const { return label; } MemPoolMeter const & MemAllocatorProxy::getMeter() const { return getAllocator()->getMeter(); } int MemAllocatorProxy::getStats(MemPoolStats * stats) { return getAllocator()->getStats(stats); } MemImplementingAllocator::MemImplementingAllocator(char const *aLabel, size_t aSize) : MemAllocator(aLabel), next(NULL), alloc_calls(0), free_calls(0), obj_size(RoundedSize(aSize)) { } MemPoolMeter const & MemImplementingAllocator::getMeter() const { return meter; } MemPoolMeter & MemImplementingAllocator::getMeter() { return meter; } size_t MemImplementingAllocator::objectSize() const { return obj_size; }