/*
 * Copyright (c) 2021-2022 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.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``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 INC. OR
 * 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 "TestHarness.h"

#if PAS_ENABLE_BMALLOC

#include "bmalloc_heap.h"
#include "bmalloc_heap_config.h"
#include <functional>
#include "pas_all_heaps.h"
#include "pas_baseline_allocator_table.h"
#include "pas_bitvector.h"
#include "pas_committed_pages_vector.h"
#include "pas_get_heap.h"
#include "pas_get_page_base.h"
#include "pas_heap_lock.h"
#include "pas_scavenger.h"
#include "pas_thread_local_cache_layout.h"
#include <thread>

using namespace std;

namespace {

size_t numCommittedPagesInTLC()
{
    pas_thread_local_cache* cache = pas_thread_local_cache_try_get();
    CHECK(cache);
    return pas_count_committed_pages(
        cache,
        pas_thread_local_cache_size_for_allocator_index_capacity(
            cache->allocator_index_capacity),
        &allocationConfig);
}

void testTLCDecommit(unsigned numHeaps,
                     function<bool(unsigned)> shouldStopAllocatorAtIndex,
                     bool commitWithFree,
                     unsigned numCommittedPagesInitially,
                     unsigned numCommittedPagesAfterDecommit,
                     unsigned numCommittedPagesAfterRecommit)
{
    pas_scavenger_suspend();
    pas_local_allocator_should_stop_count_for_suspend = 100; /* request_stop should not actually stop. */

    pas_heap_ref* heaps = new pas_heap_ref[numHeaps];
    for (size_t index = numHeaps; index--;)
        heaps[index] = BMALLOC_HEAP_REF_INITIALIZER(new bmalloc_type(BMALLOC_TYPE_INITIALIZER(42, 2, "test")));

    vector<void*> objects;
    for (size_t index = 0; index < numHeaps; ++index) {
        void* ptr = bmalloc_iso_allocate(heaps + index);
        CHECK(ptr);
        CHECK_EQUAL(pas_get_heap(ptr, BMALLOC_HEAP_CONFIG),
                    bmalloc_heap_ref_get_heap(heaps + index));
        objects.push_back(ptr);
    }

    unsigned* hasViewCache = new unsigned[PAS_BITVECTOR_NUM_WORDS(numHeaps)];
    unsigned* hasAllocator = new unsigned[PAS_BITVECTOR_NUM_WORDS(numHeaps)];

    for (size_t index = numHeaps; index--;) {
        pas_bitvector_set(hasViewCache, index, false);
        pas_bitvector_set(hasAllocator, index, false);
    }

    pas_heap_lock_lock();
    pas_thread_local_cache* cache = pas_thread_local_cache_try_get();
    CHECK(cache);
    pas_thread_local_cache_layout_node layoutNode;
    for (PAS_THREAD_LOCAL_CACHE_LAYOUT_EACH_ALLOCATOR(layoutNode)) {
        pas_allocator_index allocatorIndex;
        pas_local_allocator_scavenger_data* scavengerData;
        pas_segregated_size_directory* directory;
        pas_heap* heap;

        allocatorIndex = pas_thread_local_cache_layout_node_get_allocator_index_generic(layoutNode);

        if (allocatorIndex >= cache->allocator_index_upper_bound)
            break;

        if (!pas_thread_local_cache_layout_node_is_committed(layoutNode, cache))
            continue;

        scavengerData = static_cast<pas_local_allocator_scavenger_data*>(
            pas_thread_local_cache_get_local_allocator_direct(cache, allocatorIndex));

        directory = pas_thread_local_cache_layout_node_get_directory(layoutNode);
        heap = pas_heap_for_segregated_heap(directory->heap);

        if (!heap->heap_ref)
            continue;

        if (heap->heap_ref >= heaps && heap->heap_ref < heaps + numHeaps) {
            size_t index = heap->heap_ref - heaps;

            if (pas_thread_local_cache_layout_node_represents_allocator(layoutNode))
                pas_bitvector_set(hasAllocator, index, true);
            else {
                PAS_ASSERT(pas_thread_local_cache_layout_node_represents_view_cache(layoutNode));
                pas_bitvector_set(hasViewCache, index, true);
            }

            CHECK(!pas_local_allocator_scavenger_data_is_stopped(scavengerData));
        }
    }
    pas_heap_lock_unlock();

    for (size_t index = numHeaps; index--;) {
        CHECK(pas_bitvector_get(hasViewCache, index));
        CHECK(pas_bitvector_get(hasAllocator, index));
    }

    CHECK_EQUAL(numCommittedPagesInTLC(), numCommittedPagesInitially);

    pas_heap_lock_lock();
    cache = pas_thread_local_cache_try_get();
    CHECK(cache);
    for (PAS_THREAD_LOCAL_CACHE_LAYOUT_EACH_ALLOCATOR(layoutNode)) {
        pas_allocator_index allocatorIndex;
        pas_local_allocator_scavenger_data* scavengerData;

        allocatorIndex = pas_thread_local_cache_layout_node_get_allocator_index_generic(layoutNode);

        if (allocatorIndex >= cache->allocator_index_upper_bound)
            break;

        scavengerData = static_cast<pas_local_allocator_scavenger_data*>(
            pas_thread_local_cache_get_local_allocator_direct(cache, allocatorIndex));

        if (shouldStopAllocatorAtIndex(allocatorIndex))
            pas_local_allocator_scavenger_data_stop(scavengerData, pas_lock_lock_mode_lock, pas_lock_is_held);
    }
    pas_heap_lock_unlock();

    pas_thread_local_cache_for_all(pas_allocator_scavenge_request_stop_action,
                                   pas_deallocator_scavenge_no_action,
                                   pas_thread_local_cache_decommit_if_possible_action);

    CHECK_EQUAL(numCommittedPagesInTLC(), numCommittedPagesAfterDecommit);

    if (commitWithFree) {
        for (void* object : objects)
            bmalloc_deallocate(object);
    }

    pas_thread_local_cache_shrink(
        pas_thread_local_cache_try_get(),
        pas_lock_is_not_held);

    for (size_t index = 0; index < numHeaps; ++index) {
        void* ptr = bmalloc_iso_allocate(heaps + index);
        CHECK(ptr);
        CHECK_EQUAL(pas_get_heap(ptr, BMALLOC_HEAP_CONFIG),
                    bmalloc_heap_ref_get_heap(heaps + index));
        if (commitWithFree)
            CHECK_EQUAL(ptr, objects[index]);
    }

    CHECK_EQUAL(numCommittedPagesInTLC(), numCommittedPagesAfterRecommit);
}

void testChaosThenDecommit(unsigned numHeaps, unsigned typeSize, unsigned maxObjectsAtATime,
                           unsigned maxFromSameHeap, size_t maxSize, size_t count)
{
    static constexpr bool verbose = false;
    
    pas_heap_ref* heapRefs = new pas_heap_ref[numHeaps];
    for (unsigned index = numHeaps; index--;) {
        heapRefs[index] = BMALLOC_HEAP_REF_INITIALIZER(
            new bmalloc_type(BMALLOC_TYPE_INITIALIZER(typeSize, 1, "test")));
    }
    
    vector<vector<void*>> objects;
    size_t totalSize = 0;

    pas_heap_ref* heapRef = nullptr;

    auto selectNextHeap = [&] () {
        heapRef = heapRefs + deterministicRandomNumber(numHeaps);
    };

    selectNextHeap();

    while (count--) {
        if (!objects.empty()
            && (totalSize >= maxSize
                || deterministicRandomNumber(2))) {
            unsigned index = deterministicRandomNumber(static_cast<unsigned>(objects.size()));
            vector<void*> objectsToFree = std::move(objects[index]);
            objects[index] = std::move(objects.back());
            objects.pop_back();
            for (void* object : objectsToFree) {
                size_t size = bmalloc_get_allocation_size(object);
                CHECK_GREATER_EQUAL(totalSize, size);
                totalSize -= size;
                bmalloc_deallocate(object);
            }
        } else {
            unsigned numObjectsNow = deterministicRandomNumber(maxObjectsAtATime) + 1;
            vector<void*> objectsNow;
            while (numObjectsNow--) {
                if (!deterministicRandomNumber(maxFromSameHeap))
                    selectNextHeap();
                void* ptr = bmalloc_iso_allocate(heapRef);
                CHECK(ptr);
                CHECK_EQUAL(pas_get_heap(ptr, BMALLOC_HEAP_CONFIG),
                            bmalloc_heap_ref_get_heap(heapRef));
                objectsNow.push_back(ptr);
                totalSize += bmalloc_get_allocation_size(ptr);
            }
            objects.push_back(std::move(objectsNow));
        }
    }

    pas_scavenger_suspend();

    if (verbose)
        cout << "Running scavenger.\n";
    
    pas_scavenger_run_synchronously_now();

    if (verbose)
        cout << "Done running scavenger.\n";

    CHECK_EQUAL(numCommittedPagesInTLC(),
                pas_round_up_to_power_of_2(
                    pas_thread_local_cache_offset_of_allocator(PAS_LOCAL_ALLOCATOR_UNSELECTED_NUM_INDICES),
                    pas_page_malloc_alignment()) >> pas_page_malloc_alignment_shift());

    pas_heap_lock_lock();
    pas_all_heaps_verify_in_steady_state();
    pas_heap_lock_unlock();
}

vector<void*> prepareToTestDLCDecommitThenStuff(unsigned numHeaps)
{
    pas_heap_ref* heaps = new pas_heap_ref[numHeaps];
    for (size_t index = numHeaps; index--;)
        heaps[index] = BMALLOC_HEAP_REF_INITIALIZER(
            new bmalloc_type(BMALLOC_TYPE_INITIALIZER(512, 512, "test")));

    vector<void*> objects;
    for (size_t index = 0; index < numHeaps; ++index) {
        pas_page_base* page = nullptr;
        for (size_t objectIndex = pas_segregated_page_number_of_objects(
                 512, BMALLOC_HEAP_CONFIG.small_segregated_config, pas_segregated_page_exclusive_role);
             objectIndex--;) {
            void* ptr = bmalloc_iso_allocate(heaps + index);
            CHECK(ptr);
            CHECK_EQUAL(pas_get_heap(ptr, BMALLOC_HEAP_CONFIG),
                        bmalloc_heap_ref_get_heap(heaps + index));
            pas_page_base* myPage = pas_get_page_base(ptr, BMALLOC_HEAP_CONFIG);
            if (!page) {
                objects.push_back(ptr);
                page = myPage;
            } else
                CHECK_EQUAL(myPage, page);
            CHECK_EQUAL(pas_page_base_get_kind(myPage), pas_small_exclusive_segregated_page_kind);
        }
    }

    return objects;
}

void testTLCDecommitThenDestroyImpl(unsigned numHeaps)
{
    vector<void*> objects = prepareToTestDLCDecommitThenStuff(numHeaps);

    for (void* object : objects)
        bmalloc_deallocate(object);

    CHECK_GREATER(numCommittedPagesInTLC(), 1);
    CHECK(pas_thread_local_cache_try_get()->deallocation_log_index);

    pas_thread_local_cache_for_all(pas_allocator_scavenge_force_stop_action,
                                   pas_deallocator_scavenge_no_action,
                                   pas_thread_local_cache_decommit_if_possible_action);

    CHECK_EQUAL(numCommittedPagesInTLC(), 1);
    CHECK(pas_thread_local_cache_try_get()->deallocation_log_index);
}

void testTLCDecommitThenDestroy(unsigned numHeaps)
{
    pas_scavenger_suspend();

    testTLCDecommitThenDestroyImpl(numHeaps);

    pas_thread_local_cache_destroy(pas_lock_is_not_held);
}

void testTLCDecommitThenDestroyInThread(unsigned numHeaps)
{
    pas_scavenger_suspend();

    thread myThread = thread([&] () {
        testTLCDecommitThenDestroyImpl(numHeaps);
    });

    myThread.join();
}

void testTLCDecommitThenFlush(unsigned numHeaps)
{
    pas_scavenger_suspend();

    testTLCDecommitThenDestroyImpl(numHeaps);
    pas_thread_local_cache_flush_deallocation_log(pas_thread_local_cache_try_get(), pas_lock_is_not_held);
}

void testTLCDecommitThenDeallocate(unsigned numHeaps)
{
    pas_scavenger_suspend();

    vector<void*> objects = prepareToTestDLCDecommitThenStuff(numHeaps);

    CHECK_GREATER(numCommittedPagesInTLC(), 1);
    CHECK(!pas_thread_local_cache_try_get()->deallocation_log_index);

    pas_thread_local_cache_for_all(pas_allocator_scavenge_force_stop_action,
                                   pas_deallocator_scavenge_no_action,
                                   pas_thread_local_cache_decommit_if_possible_action);

    for (void* object : objects)
        bmalloc_deallocate(object);
    CHECK(pas_thread_local_cache_try_get()->deallocation_log_index);
    pas_thread_local_cache_flush_deallocation_log(pas_thread_local_cache_try_get(), pas_lock_is_not_held);
}

void testAllocateFromStoppedBaselineImpl()
{
    static constexpr bool verbose = false;

    if (verbose) {
        cout << "TLC = " << pas_thread_local_cache_try_get() << "\n";
        cout << "can set TLC = " << pas_thread_local_cache_can_set() << "\n";
    }

    pas_heap_ref heapRef = BMALLOC_HEAP_REF_INITIALIZER(
        new bmalloc_type(BMALLOC_TYPE_INITIALIZER(32, 1, "test")));

    void* ptr = bmalloc_iso_allocate(&heapRef);
    CHECK(ptr);
    pas_segregated_view view = pas_segregated_view_for_object(
        reinterpret_cast<uintptr_t>(ptr), &bmalloc_heap_config);
    pas_segregated_size_directory* directory = pas_segregated_view_get_size_directory(view);

    bool foundInBaselineTable = false;
    unsigned foundIndex = UINT_MAX;
    for (unsigned i = pas_baseline_allocator_table_bound; i--;) {
        if (pas_segregated_view_get_size_directory(pas_baseline_allocator_table[i].u.allocator.view)
            == directory) {
            foundInBaselineTable = true;
            foundIndex = i;
            break;
        }
    }
    CHECK(foundInBaselineTable);

    pas_baseline_allocator_table_for_all(pas_allocator_scavenge_force_stop_action);

    CHECK_EQUAL(pas_baseline_allocator_table[foundIndex].u.allocator.scavenger_data.kind,
                pas_local_allocator_stopped_allocator_kind);

    if (verbose)
        cout << "TLC = " << pas_thread_local_cache_try_get() << "\n";

    ptr = bmalloc_iso_allocate(&heapRef);
    CHECK(ptr);
}

void testAllocateFromStoppedBaseline()
{
    pas_scavenger_suspend();
    testAllocateFromStoppedBaselineImpl();
}

pthread_key_t testAllocateFromStoppedBaselineDuringThreadDestructionKey;

void testAllocateFromStoppedBaselineDuringThreadDestructionDestruct(void*)
{
    testAllocateFromStoppedBaselineImpl();
    int result = pthread_setspecific(testAllocateFromStoppedBaselineDuringThreadDestructionKey, "hello");
    CHECK_EQUAL(result, 0);
}

void testAllocateFromStoppedBaselineDuringThreadDestruction()
{
    pas_scavenger_suspend();

    thread myThread([&] () {
        int result = pthread_key_create(&testAllocateFromStoppedBaselineDuringThreadDestructionKey,
                                        testAllocateFromStoppedBaselineDuringThreadDestructionDestruct);
        CHECK_EQUAL(result, 0);
        result = pthread_setspecific(testAllocateFromStoppedBaselineDuringThreadDestructionKey, "hello");
        CHECK_EQUAL(result, 0);
    });

    myThread.join();
}

} // anonymous namespace

#endif // PAS_ENABLE_BMALLOC

void addTLCDecommitTests()
{
#if PAS_ENABLE_BMALLOC
    {
        DecommitZeroFill decommitZeroFill;
    
        TestScope testScope(
            "rage-tlcs",
            [] () {
                bmalloc_typed_runtime_config.base.directory_size_bound_for_baseline_allocators = 0;
                bmalloc_typed_runtime_config.base.directory_size_bound_for_no_view_cache = 0;
                bmalloc_typed_runtime_config.base.view_cache_capacity_for_object_size =
                    pas_heap_runtime_config_aggressive_view_cache_capacity;
            });
    
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index < 10000; }, false,
                                 588, 300, 304));
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index < 10000; }, true,
                                 588, 300, 304));
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index > 10000; }, false,
                                 588, 6, 304));
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index > 10000; }, true,
                                 588, 6, 304));
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index < 100000; }, false,
                                 588, 256, 304));
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index < 100000; }, true,
                                 588, 256, 304));
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index > 100000; }, false,
                                 588, 50, 304));
        ADD_TEST(testTLCDecommit(10000, [] (unsigned index) { return index > 100000; }, true,
                                 588, 50, 304));
        ADD_TEST(testChaosThenDecommit(10000, 16, 1000, 100, 400000000, 500000));
        ADD_TEST(testChaosThenDecommit(10000, 16, 1000, 10000, 400000000, 1000000));
        ADD_TEST(testChaosThenDecommit(10000, 512, 10, 50, 500000000, 3000000));
        ADD_TEST(testChaosThenDecommit(10000, 48, 100, 1000, 600000000, 3000000));
        ADD_TEST(testChaosThenDecommit(10000, 48, 100, 10000, 600000000, 3000000));

        {
            ForceExclusives forceExclusives;
            ADD_TEST(testTLCDecommitThenDeallocate(6666));
            ADD_TEST(testTLCDecommitThenFlush(6666));
            ADD_TEST(testTLCDecommitThenDestroy(6666));
            ADD_TEST(testTLCDecommitThenDestroyInThread(6666));
        }
    }

    {
        ForceBaselines forceBaselines;
        ADD_TEST(testAllocateFromStoppedBaseline());
        ADD_TEST(testAllocateFromStoppedBaselineDuringThreadDestruction());
    }
#endif // PAS_ENABLE_BMALLOC
}
