/*
 * Copyright (C) 2011-2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer. 
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution. 
 * 3.  Neither the name of Apple Inc. ("Apple") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include <wtf/MetaAllocator.h>

#include <wtf/NeverDestroyed.h>
#include <wtf/TZoneMallocInlines.h>
#include <wtf/WTFConfig.h>

WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN

namespace WTF {

WTF_MAKE_COMPACT_TZONE_ALLOCATED_IMPL(MetaAllocatorHandle);
DEFINE_ALLOCATOR_WITH_HEAP_IDENTIFIER(MetaAllocatorFreeSpace);

MetaAllocator::~MetaAllocator()
{
    // Clear CheckedPtrs in these maps before we start freeing nodes.
    m_freeSpaceStartAddressMap.clear();
    m_freeSpaceEndAddressMap.clear();

    for (CheckedPtr node = m_freeSpaceSizeMap.first(); node;) {
        CheckedPtr next = node->successor();
        m_freeSpaceSizeMap.remove(node.get());
        freeFreeSpaceNode(WTF::move(node));
        node = WTF::move(next);
    }
#ifndef NDEBUG
    ASSERT(!m_mallocBalance);
#endif
}

void MetaAllocatorTracker::notify(MetaAllocatorHandle& handle)
{
    m_allocations.insert(&handle);
}

void MetaAllocatorTracker::release(MetaAllocatorHandle& handle)
{
    m_allocations.remove(&handle);
}

void MetaAllocator::release(const Locker<Lock>&, MetaAllocatorHandle& handle)
{
    if (handle.sizeInBytes()) {
        MemoryPtr start = handle.start();
        size_t sizeInBytes = handle.sizeInBytes();
        decrementPageOccupancy(start.untaggedPtr(), sizeInBytes);
        addFreeSpaceFromReleasedHandle(start.retagged<FreeSpacePtrTag>(), sizeInBytes);
    }

    if (!!m_tracker) [[unlikely]]
        m_tracker->release(handle);
}

MetaAllocatorHandle::MetaAllocatorHandle(MetaAllocator& allocator, MetaAllocatorHandle::MemoryPtr start, size_t sizeInBytes)
    : m_allocator(allocator)
    , m_start(start)
    , m_end(start + sizeInBytes)
{
    ASSERT(start);
    ASSERT(sizeInBytes);
}

MetaAllocatorHandle::~MetaAllocatorHandle()
{
    Locker locker { allocator().m_lock };
    allocator().release(locker, *this);
}

void MetaAllocatorHandle::shrink(size_t newSizeInBytes)
{
    size_t sizeInBytes = this->sizeInBytes();
    ASSERT(newSizeInBytes <= sizeInBytes);

    MetaAllocator& allocator = this->allocator();
    Locker locker { allocator.m_lock };

    newSizeInBytes = allocator.roundUp(newSizeInBytes);
    
    ASSERT(newSizeInBytes <= sizeInBytes);

    if (newSizeInBytes == sizeInBytes)
        return;

    MemoryPtr freeStart = m_start + newSizeInBytes;
    size_t freeSize = sizeInBytes - newSizeInBytes;
    uintptr_t freeStartValue = freeStart.untaggedPtr<uintptr_t>();
    uintptr_t freeEnd = freeStartValue + freeSize;
    
    uintptr_t firstCompletelyFreePage = roundUpToMultipleOf(allocator.m_pageSize, freeStartValue);
    if (firstCompletelyFreePage < freeEnd)
        allocator.decrementPageOccupancy(reinterpret_cast<void*>(firstCompletelyFreePage), freeSize - (firstCompletelyFreePage - freeStartValue));

    allocator.addFreeSpaceFromReleasedHandle(freeStart.retagged<FreeSpacePtrTag>(), freeSize);

    m_end = freeStart;
}

void MetaAllocatorHandle::dump(PrintStream& out) const
{
    out.print(RawPointer(start().untaggedPtr()), "...", RawPointer(end().untaggedPtr()));
}

MetaAllocator::MetaAllocator(Lock& lock, size_t allocationGranule, size_t pageSize)
    : m_allocationGranule(allocationGranule)
    , m_pageSize(pageSize)
    , m_bytesAllocated(0)
    , m_bytesReserved(0)
    , m_bytesCommitted(0)
    , m_lock(lock)
#ifndef NDEBUG
    , m_mallocBalance(0)
#endif
#if ENABLE(META_ALLOCATOR_PROFILE)
    , m_numAllocations(0)
    , m_numFrees(0)
#endif
{
    for (m_logPageSize = 0; m_logPageSize < 32; ++m_logPageSize) {
        if (static_cast<size_t>(1) << m_logPageSize == m_pageSize)
            break;
    }
    
    ASSERT(static_cast<size_t>(1) << m_logPageSize == m_pageSize);
    
    for (m_logAllocationGranule = 0; m_logAllocationGranule < 32; ++m_logAllocationGranule) {
        if (static_cast<size_t>(1) << m_logAllocationGranule == m_allocationGranule)
            break;
    }
    
    ASSERT(static_cast<size_t>(1) << m_logAllocationGranule == m_allocationGranule);
}

RefPtr<MetaAllocatorHandle> MetaAllocator::allocate(const Locker<Lock>&, size_t sizeInBytes)
{
    if (!sizeInBytes)
        return nullptr;
    
    sizeInBytes = roundUp(sizeInBytes);

    FreeSpacePtr start = findAndRemoveFreeSpace(sizeInBytes);
    if (!start) {
        size_t requestedNumberOfPages = (sizeInBytes + m_pageSize - 1) >> m_logPageSize;
        size_t numberOfPages = requestedNumberOfPages;
        
        start = allocateNewSpace(numberOfPages);
        if (!start)
            return nullptr;
        
        ASSERT(numberOfPages >= requestedNumberOfPages);
        
        size_t roundedUpSize = numberOfPages << m_logPageSize;
        
        ASSERT(roundedUpSize >= sizeInBytes);
        
        m_bytesReserved += roundedUpSize;
        
        if (roundedUpSize > sizeInBytes) {
            FreeSpacePtr freeSpaceStart = start + sizeInBytes;
            size_t freeSpaceSize = roundedUpSize - sizeInBytes;
            addFreeSpace(freeSpaceStart, freeSpaceSize);
        }
    }
    incrementPageOccupancy(start.untaggedPtr(), sizeInBytes);
    m_bytesAllocated += sizeInBytes;
#if ENABLE(META_ALLOCATOR_PROFILE)
    m_numAllocations++;
#endif

    auto handle = adoptRef(*new MetaAllocatorHandle(*this, start.retagged<HandleMemoryPtrTag>(), sizeInBytes));

    if (!!m_tracker) [[unlikely]]
        m_tracker->notify(*handle.ptr());

    return handle;
}

MetaAllocator::Statistics MetaAllocator::currentStatistics(const Locker<Lock>&)
{
    Statistics result;
    result.bytesAllocated = m_bytesAllocated;
    result.bytesReserved = m_bytesReserved;
    result.bytesCommitted = m_bytesCommitted;
    return result;
}

MetaAllocator::FreeSpacePtr MetaAllocator::findAndRemoveFreeSpace(size_t sizeInBytes)
{
    CheckedPtr node = m_freeSpaceSizeMap.findLeastGreaterThanOrEqual(sizeInBytes);
    
    if (!node)
        return nullptr;
    
    size_t nodeSizeInBytes = node->sizeInBytes();
    RELEASE_ASSERT(nodeSizeInBytes >= sizeInBytes);

    m_freeSpaceSizeMap.remove(node.get());

    FreeSpacePtr result;

    if (nodeSizeInBytes == sizeInBytes) {
        // Easy case: perfect fit, so just remove the node entirely.
        result = node->m_start;
        
        m_freeSpaceStartAddressMap.remove(node->m_start);
        m_freeSpaceEndAddressMap.remove(node->m_end);
        freeFreeSpaceNode(WTF::move(node));
    } else {
        // Try to be a good citizen and ensure that the returned chunk of memory
        // straddles as few pages as possible, but only insofar as doing so will
        // not increase fragmentation. The intuition is that minimizing
        // fragmentation is a strictly higher priority than minimizing the number
        // of committed pages, since in the long run, smaller fragmentation means
        // fewer committed pages and fewer failures in general.
        
        uintptr_t nodeStartAsInt = node->m_start.untaggedPtr<uintptr_t>();
        uintptr_t firstPage = nodeStartAsInt >> m_logPageSize;
        uintptr_t lastPage = (nodeStartAsInt + nodeSizeInBytes - 1) >> m_logPageSize;

        uintptr_t lastPageForLeftAllocation = (nodeStartAsInt + sizeInBytes - 1) >> m_logPageSize;
        uintptr_t firstPageForRightAllocation = (nodeStartAsInt + nodeSizeInBytes - sizeInBytes) >> m_logPageSize;
        
        if (lastPageForLeftAllocation - firstPage + 1 <= lastPage - firstPageForRightAllocation + 1) {
            // Allocate in the left side of the returned chunk, and slide the node to the right.
            result = node->m_start;
            
            m_freeSpaceStartAddressMap.remove(node->m_start);

            node->m_start += sizeInBytes;
            RELEASE_ASSERT(nodeStartAsInt < node->m_start.untaggedPtr<uintptr_t>() && node->m_start.untaggedPtr<uintptr_t>() < node->m_end.untaggedPtr<uintptr_t>());

            m_freeSpaceSizeMap.insert(node.get());
            m_freeSpaceStartAddressMap.add(node->m_start, node.get());
        } else {
            // Allocate in the right size of the returned chunk, and slide the node to the left;

            result = node->m_end - sizeInBytes;

            m_freeSpaceEndAddressMap.remove(node->m_end);

            node->m_end = result;

            m_freeSpaceSizeMap.insert(node.get());
            m_freeSpaceEndAddressMap.add(result, node.get());
        }
    }
    
#if ENABLE(META_ALLOCATOR_PROFILE)
    dumpProfile();
#endif

    return result;
}

void MetaAllocator::addFreeSpaceFromReleasedHandle(FreeSpacePtr start, size_t sizeInBytes)
{
#if ENABLE(META_ALLOCATOR_PROFILE)
    m_numFrees++;
#endif
    m_bytesAllocated -= sizeInBytes;
    addFreeSpace(start, sizeInBytes);
}

void MetaAllocator::addFreshFreeSpace(void* start, size_t sizeInBytes)
{
    Config::AssertNotFrozenScope assertNotFrozenScope;
    Locker locker { m_lock };
    m_bytesReserved += sizeInBytes;
    addFreeSpace(FreeSpacePtr::fromUntaggedPtr(start), sizeInBytes);
}

size_t MetaAllocator::debugFreeSpaceSize()
{
#ifndef NDEBUG
    Locker locker { m_lock };
    size_t result = 0;
    for (CheckedPtr node = m_freeSpaceSizeMap.first(); node; node = node->successor())
        result += node->sizeInBytes();
    return result;
#else
    CRASH();
    return 0;
#endif
}

void MetaAllocator::addFreeSpace(FreeSpacePtr start, size_t sizeInBytes)
{
    FreeSpacePtr end = start + sizeInBytes;

    auto leftNeighbor = m_freeSpaceEndAddressMap.find(start);
    auto rightNeighbor = m_freeSpaceStartAddressMap.find(end);

    if (leftNeighbor != m_freeSpaceEndAddressMap.end()) {
        // We have something we can coalesce with on the left. Remove it from the tree, and
        // remove its end from the end address map.
        
        ASSERT(leftNeighbor->value->m_end == leftNeighbor->key);

        CheckedPtr leftNode = leftNeighbor->value;

        FreeSpacePtr leftEnd = leftNode->m_end;

        ASSERT(leftEnd == start);
        
        m_freeSpaceSizeMap.remove(leftNode.get());
        m_freeSpaceEndAddressMap.remove(leftEnd);
        
        // Now check if there is also something to coalesce with on the right.
        if (rightNeighbor != m_freeSpaceStartAddressMap.end()) {
            // Freeing something in the middle of free blocks. Coalesce both left and
            // right, whilst removing the right neighbor from the maps.
            
            ASSERT(rightNeighbor->value->m_start == rightNeighbor->key);
            
            CheckedPtr rightNode = rightNeighbor->value;
            FreeSpacePtr rightStart = rightNeighbor->key;
            size_t rightSize = rightNode->sizeInBytes();
            FreeSpacePtr rightEnd = rightNode->m_end;

            ASSERT(rightStart == end);
            ASSERT(leftNode->m_start + (leftNode->sizeInBytes() + sizeInBytes + rightSize) == rightEnd);

            m_freeSpaceSizeMap.remove(rightNode.get());
            m_freeSpaceStartAddressMap.remove(rightStart);
            m_freeSpaceEndAddressMap.remove(rightEnd);

            leftNode->m_end += (sizeInBytes + rightSize);

            m_freeSpaceSizeMap.insert(leftNode.get());
            m_freeSpaceEndAddressMap.add(rightEnd, leftNode.get());

            freeFreeSpaceNode(WTF::move(rightNode));
        } else {
            leftNode->m_end += sizeInBytes;

            m_freeSpaceSizeMap.insert(leftNode.get());
            m_freeSpaceEndAddressMap.add(end, leftNode.get());
        }
    } else {
        // Cannot coalesce with left; try to see if we can coalesce with right.
        
        if (rightNeighbor != m_freeSpaceStartAddressMap.end()) {
            CheckedPtr rightNode = rightNeighbor->value;
            FreeSpacePtr rightStart = rightNeighbor->key;

            ASSERT(rightStart == end);
            ASSERT(start + (sizeInBytes + rightNode->sizeInBytes()) == rightNode->m_end);

            m_freeSpaceSizeMap.remove(rightNode.get());
            m_freeSpaceStartAddressMap.remove(rightStart);

            rightNode->m_start = start;

            m_freeSpaceSizeMap.insert(rightNode.get());
            m_freeSpaceStartAddressMap.add(start, rightNode.get());
        } else {
            // Nothing to coalesce with, so create a new free space node and add it.
            
            CheckedPtr node = allocFreeSpaceNode();

            node->m_start = start;
            node->m_end = start + sizeInBytes;

            m_freeSpaceSizeMap.insert(node.get());
            m_freeSpaceStartAddressMap.add(start, node.get());
            m_freeSpaceEndAddressMap.add(end, node.get());
        }
    }
    
#if ENABLE(META_ALLOCATOR_PROFILE)
    dumpProfile();
#endif
}

void MetaAllocator::incrementPageOccupancy(void* address, size_t sizeInBytes)
{
    uintptr_t firstPage = reinterpret_cast<uintptr_t>(address) >> m_logPageSize;
    uintptr_t lastPage = (reinterpret_cast<uintptr_t>(address) + sizeInBytes - 1) >> m_logPageSize;

    uintptr_t currentPageStart = 0;
    size_t count = 0;
    auto flushNeedPages = [&] {
        if (!currentPageStart)
            return;
        notifyNeedPage(reinterpret_cast<void*>(currentPageStart << m_logPageSize), count);
        currentPageStart = 0;
        count = 0;
    };
    
    for (uintptr_t page = firstPage; page <= lastPage; ++page) {
        auto result = m_pageOccupancyMap.add(page, 1);
        if (result.isNewEntry) {
            m_bytesCommitted += m_pageSize;
            if (!currentPageStart)
                currentPageStart = page;
            ++count;
        } else {
            result.iterator->value++;
            flushNeedPages();
        }
    }
    flushNeedPages();
}

void MetaAllocator::decrementPageOccupancy(void* address, size_t sizeInBytes)
{
    uintptr_t firstPage = reinterpret_cast<uintptr_t>(address) >> m_logPageSize;
    uintptr_t lastPage = (reinterpret_cast<uintptr_t>(address) + sizeInBytes - 1) >> m_logPageSize;

    uintptr_t currentPageStart = 0;
    size_t count = 0;
    auto flushFreePages = [&] {
        if (!currentPageStart)
            return;
        notifyPageIsFree(reinterpret_cast<void*>(currentPageStart << m_logPageSize), count);
        currentPageStart = 0;
        count = 0;
    };
    
    for (uintptr_t page = firstPage; page <= lastPage; ++page) {
        UncheckedKeyHashMap<uintptr_t, size_t>::iterator iter = m_pageOccupancyMap.find(page);
        ASSERT(iter != m_pageOccupancyMap.end());
        if (!--(iter->value)) {
            m_pageOccupancyMap.remove(iter);
            m_bytesCommitted -= m_pageSize;
            if (!currentPageStart)
                currentPageStart = page;
            ++count;
        } else
            flushFreePages();
    }
    flushFreePages();
}

bool MetaAllocator::isInAllocatedMemory(const AbstractLocker&, void* address)
{
    ASSERT(m_lock.isLocked());
    uintptr_t page = reinterpret_cast<uintptr_t>(address) >> m_logPageSize;
    return m_pageOccupancyMap.contains(page);
}

size_t MetaAllocator::roundUp(size_t sizeInBytes)
{
    if (std::numeric_limits<size_t>::max() - m_allocationGranule <= sizeInBytes)
        CRASH();
    return (sizeInBytes + m_allocationGranule - 1) & ~(m_allocationGranule - 1);
}

MetaAllocator::FreeSpaceNode* MetaAllocator::allocFreeSpaceNode()
{
#ifndef NDEBUG
    m_mallocBalance++;
#endif
    return new FreeSpaceNode;
}

void MetaAllocator::freeFreeSpaceNode(CheckedPtr<FreeSpaceNode>&& node)
{
#ifndef NDEBUG
    m_mallocBalance--;
#endif
    SUPPRESS_UNCHECKED_LOCAL auto* nodePtr = std::exchange(node, nullptr).unsafeGet(); // NOLINT
    delete nodePtr;
}

#if ENABLE(META_ALLOCATOR_PROFILE)
void MetaAllocator::dumpProfile()
{
    dataLogF(
        "%d: MetaAllocator(%p): num allocations = %u, num frees = %u, allocated = %lu, reserved = %lu, committed = %lu\n",
        getCurrentProcessID(), this, m_numAllocations, m_numFrees, m_bytesAllocated, m_bytesReserved, m_bytesCommitted);
}
#endif

} // namespace WTF

WTF_ALLOW_UNSAFE_BUFFER_USAGE_END
