root/src/kits/media/RealtimeAlloc.cpp
/*
 * Copyright 2009, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


/*!     A simple allocator that works directly on an area, based on the boot
        loader's heap. See there for more information about its inner workings.
*/


#include <RealtimeAlloc.h>

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <OS.h>

#include <locks.h>
#include <util/DoublyLinkedList.h>


//#define TRACE_RTM
#ifdef TRACE_RTM
#       define TRACE(x...) printf(x);
#else
#       define TRACE(x...) ;
#endif


class FreeChunk {
public:
                        void                            SetTo(size_t size, FreeChunk* next);

                        uint32                          Size() const;
                        uint32                          CompleteSize() const { return fSize; }

                        FreeChunk*                      Next() const { return fNext; }
                        void                            SetNext(FreeChunk* next) { fNext = next; }

                        FreeChunk*                      Split(uint32 splitSize);
                        bool                            IsTouching(FreeChunk* link);
                        FreeChunk*                      Join(FreeChunk* link);
                        void                            Remove(rtm_pool* pool,
                                                                        FreeChunk* previous = NULL);
                        void                            Enqueue(rtm_pool* pool);

                        void*                           AllocatedAddress() const;
        static  FreeChunk*                      SetToAllocated(void* allocated);
        static  addr_t                          NextOffset() { return sizeof(size_t); }

private:
                        size_t                          fSize;
                        FreeChunk*                      fNext;
};


struct rtm_pool : DoublyLinkedListLinkImpl<rtm_pool> {
        area_id         area;
        void*           heap_base;
        size_t          max_size;
        size_t          available;
        FreeChunk       free_anchor;
        mutex           lock;

        bool Contains(void* buffer) const;
        void Free(void* buffer);
};

typedef DoublyLinkedList<rtm_pool> PoolList;


const static uint32 kAlignment = 256;
        // all memory chunks will be a multiple of this

static mutex sPoolsLock = MUTEX_INITIALIZER("rtm pools");
static PoolList sPools;


void
FreeChunk::SetTo(size_t size, FreeChunk* next)
{
        fSize = size;
        fNext = next;
}


/*!     Returns the amount of bytes that can be allocated
        in this chunk.
*/
uint32
FreeChunk::Size() const
{
        return fSize - FreeChunk::NextOffset();
}


/*!     Splits the upper half at the requested location
        and returns it.
*/
FreeChunk*
FreeChunk::Split(uint32 splitSize)
{
        splitSize = (splitSize - 1 + kAlignment) & ~(kAlignment - 1);

        FreeChunk* chunk
                = (FreeChunk*)((uint8*)this + FreeChunk::NextOffset() + splitSize);
        chunk->fSize = fSize - splitSize - FreeChunk::NextOffset();
        chunk->fNext = fNext;

        fSize = splitSize + FreeChunk::NextOffset();

        return chunk;
}


/*!     Checks if the specified chunk touches this chunk, so
        that they could be joined.
*/
bool
FreeChunk::IsTouching(FreeChunk* chunk)
{
        return chunk
                && (((uint8*)this + fSize == (uint8*)chunk)
                        || (uint8*)chunk + chunk->fSize == (uint8*)this);
}


/*!     Joins the chunk to this chunk and returns the pointer
        to the new chunk - which will either be one of the
        two chunks.
        Note, the chunks must be joinable, or else this method
        doesn't work correctly. Use FreeChunk::IsTouching()
        to check if this method can be applied.
*/
FreeChunk*
FreeChunk::Join(FreeChunk* chunk)
{
        if (chunk < this) {
                chunk->fSize += fSize;
                chunk->fNext = fNext;

                return chunk;
        }

        fSize += chunk->fSize;
        fNext = chunk->fNext;

        return this;
}


void
FreeChunk::Remove(rtm_pool* pool, FreeChunk* previous)
{
        if (previous == NULL) {
                // find the previous chunk in the list
                FreeChunk* chunk = pool->free_anchor.fNext;

                while (chunk != NULL && chunk != this) {
                        previous = chunk;
                        chunk = chunk->fNext;
                }

                if (chunk == NULL)
                        return;
        }

        previous->fNext = fNext;
        fNext = NULL;
}


void
FreeChunk::Enqueue(rtm_pool* pool)
{
        FreeChunk* chunk = pool->free_anchor.fNext;
        FreeChunk* last = &pool->free_anchor;
        while (chunk && chunk->Size() < fSize) {
                last = chunk;
                chunk = chunk->fNext;
        }

        fNext = chunk;
        last->fNext = this;
}


void*
FreeChunk::AllocatedAddress() const
{
        return (void*)&fNext;
}


FreeChunk*
FreeChunk::SetToAllocated(void* allocated)
{
        return (FreeChunk*)((addr_t)allocated - FreeChunk::NextOffset());
}


// #pragma mark - rtm_pool


bool
rtm_pool::Contains(void* buffer) const
{
        return (addr_t)heap_base <= (addr_t)buffer
                && (addr_t)heap_base - 1 + max_size >= (addr_t)buffer;
}


void
rtm_pool::Free(void* allocated)
{
        FreeChunk* freedChunk = FreeChunk::SetToAllocated(allocated);
        available += freedChunk->CompleteSize();

        // try to join the new free chunk with an existing one
        // it may be joined with up to two chunks

        FreeChunk* chunk = free_anchor.Next();
        FreeChunk* last = &free_anchor;
        int32 joinCount = 0;

        while (chunk) {
                if (chunk->IsTouching(freedChunk)) {
                        // almost "insert" it into the list before joining
                        // because the next pointer is inherited by the chunk
                        freedChunk->SetNext(chunk->Next());
                        freedChunk = chunk->Join(freedChunk);

                        // remove the joined chunk from the list
                        last->SetNext(freedChunk->Next());
                        chunk = last;

                        if (++joinCount == 2)
                                break;
                }

                last = chunk;
                chunk = chunk->Next();
        }

        // enqueue the link at the right position; the
        // free link queue is ordered by size

        freedChunk->Enqueue(this);
}


// #pragma mark -


static rtm_pool*
pool_for(void* buffer)
{
        MutexLocker _(&sPoolsLock);

        PoolList::Iterator iterator = sPools.GetIterator();
        while (rtm_pool* pool = iterator.Next()) {
                if (pool->Contains(buffer))
                        return pool;
        }

        return NULL;
}


// #pragma mark - public API


status_t
rtm_create_pool(rtm_pool** _pool, size_t totalSize, const char* name)
{
        rtm_pool* pool = (rtm_pool*)malloc(sizeof(rtm_pool));
        if (pool == NULL)
                return B_NO_MEMORY;

        if (name != NULL)
                mutex_init_etc(&pool->lock, name, MUTEX_FLAG_CLONE_NAME);
        else
                mutex_init(&pool->lock, "realtime pool");

        // Allocate enough space for at least one allocation over \a totalSize
        pool->max_size = (totalSize + sizeof(FreeChunk) - 1 + B_PAGE_SIZE)
                & ~(B_PAGE_SIZE - 1);

        area_id area = create_area(name, &pool->heap_base, B_ANY_ADDRESS,
                pool->max_size, B_LAZY_LOCK, B_READ_AREA | B_WRITE_AREA);
        if (area < 0) {
                mutex_destroy(&pool->lock);
                free(pool);
                return area;
        }

        pool->area = area;
        pool->available = pool->max_size - FreeChunk::NextOffset();

        // declare the whole heap as one chunk, and add it
        // to the free list

        FreeChunk* chunk = (FreeChunk*)pool->heap_base;
        chunk->SetTo(pool->max_size, NULL);

        pool->free_anchor.SetTo(0, chunk);

        *_pool = pool;

        MutexLocker _(&sPoolsLock);
        sPools.Add(pool);
        return B_OK;
}


status_t
rtm_delete_pool(rtm_pool* pool)
{
        if (pool == NULL)
                return B_BAD_VALUE;

        mutex_lock(&pool->lock);

        {
                MutexLocker _(&sPoolsLock);
                sPools.Remove(pool);
        }

        delete_area(pool->area);
        mutex_destroy(&pool->lock);
        free(pool);

        return B_OK;
}


void*
rtm_alloc(rtm_pool* pool, size_t size)
{
        if (pool == NULL)
                return malloc(size);

        if (pool->heap_base == NULL || size == 0)
                return NULL;

        MutexLocker _(&pool->lock);

        // align the size requirement to a kAlignment bytes boundary
        size = (size - 1 + kAlignment) & ~(size_t)(kAlignment - 1);

        if (size > pool->available) {
                TRACE("malloc(): Out of memory!\n");
                return NULL;
        }

        FreeChunk* chunk = pool->free_anchor.Next();
        FreeChunk* last = &pool->free_anchor;
        while (chunk && chunk->Size() < size) {
                last = chunk;
                chunk = chunk->Next();
        }

        if (chunk == NULL) {
                // could not find a free chunk as large as needed
                TRACE("malloc(): Out of memory!\n");
                return NULL;
        }

        if (chunk->Size() > size + sizeof(FreeChunk) + kAlignment) {
                // if this chunk is bigger than the requested size,
                // we split it to form two chunks (with a minimal
                // size of kAlignment allocatable bytes).

                FreeChunk* freeChunk = chunk->Split(size);
                last->SetNext(freeChunk);

                // re-enqueue the free chunk at the correct position
                freeChunk->Remove(pool, last);
                freeChunk->Enqueue(pool);
        } else {
                // remove the chunk from the free list

                last->SetNext(chunk->Next());
        }

        pool->available -= size + sizeof(size_t);

        TRACE("malloc(%lu) -> %p\n", size, chunk->AllocatedAddress());
        return chunk->AllocatedAddress();
}


status_t
rtm_free(void* allocated)
{
        if (allocated == NULL)
                return B_OK;

        TRACE("rtm_free(%p)\n", allocated);

        // find pool
        rtm_pool* pool = pool_for(allocated);
        if (pool == NULL) {
                free(allocated);
                return B_OK;
        }

        MutexLocker _(&pool->lock);
        pool->Free(allocated);
        return B_OK;
}


status_t
rtm_realloc(void** _buffer, size_t newSize)
{
        if (_buffer == NULL)
                return B_BAD_VALUE;

        TRACE("rtm_realloc(%p, %lu)\n", *_buffer, newSize);

        void* oldBuffer = *_buffer;

        // find pool
        rtm_pool* pool = pool_for(oldBuffer);
        if (pool == NULL) {
                void* buffer = realloc(oldBuffer, newSize);
                if (buffer != NULL) {
                        *_buffer = buffer;
                        return B_OK;
                }
                return B_NO_MEMORY;
        }

        MutexLocker _(&pool->lock);

        if (newSize == 0) {
                TRACE("realloc(%p, %lu) -> NULL\n", oldBuffer, newSize);
                pool->Free(oldBuffer);
                *_buffer = NULL;
                return B_OK;
        }

        size_t copySize = newSize;
        if (oldBuffer != NULL) {
                FreeChunk* oldChunk = FreeChunk::SetToAllocated(oldBuffer);

                // Check if the old buffer still fits, and if it makes sense to keep it
                if (oldChunk->Size() >= newSize && newSize > oldChunk->Size() / 3) {
                        TRACE("realloc(%p, %lu) old buffer is large enough\n",
                                oldBuffer, newSize);
                        return B_OK;
                }

                if (copySize > oldChunk->Size())
                        copySize = oldChunk->Size();
        }

        void* newBuffer = rtm_alloc(pool, newSize);
        if (newBuffer == NULL)
                return B_NO_MEMORY;

        if (oldBuffer != NULL) {
                memcpy(newBuffer, oldBuffer, copySize);
                pool->Free(oldBuffer);
        }

        TRACE("realloc(%p, %lu) -> %p\n", oldBuffer, newSize, newBuffer);
        *_buffer = newBuffer;
        return B_OK;
}


status_t
rtm_size_for(void* buffer)
{
        if (buffer == NULL)
                return 0;

        FreeChunk* chunk = FreeChunk::SetToAllocated(buffer);
        // TODO: we currently always return the actual chunk size, not the allocated
        // one
        return chunk->Size();
}


status_t
rtm_phys_size_for(void* buffer)
{
        if (buffer == NULL)
                return 0;

        FreeChunk* chunk = FreeChunk::SetToAllocated(buffer);
        return chunk->Size();
}


size_t
rtm_available(rtm_pool* pool)
{
        if (pool == NULL) {
                // whatever - might want to use system_info instead
                return 1024 * 1024;
        }

        return pool->available;
}


rtm_pool*
rtm_default_pool()
{
        // We always return NULL - the default pool will just use malloc()/free()
        return NULL;
}


#if 0
extern "C" {

// undocumented symbols that BeOS exports
status_t rtm_create_pool_etc(rtm_pool ** out_pool, size_t total_size, const char * name, int32 param4, int32 param5, ...);
void rtm_get_pool(rtm_pool *pool,void *data,int32 param3,int32 param4, ...);

}
#endif