/*
 * Copyright (c) 2020, 2023 SAP SE. All rights reserved.
 * Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 *
 */

import jdk.test.whitebox.WhiteBox;

import java.util.HashSet;

public class MetaspaceTestContext {

    long context;

    final long commitLimit;
    final long reserveLimit;

    int numArenasCreated;
    int numArenasDestroyed;

    HashSet<MetaspaceTestArena> arenaList = new HashSet<>();

    long allocatedBytes;
    long numAllocated;
    long deallocatedBytes;
    long numDeallocated;
    long allocationFailures;

    public MetaspaceTestContext(long commitLimit, long reserveLimit) {
        this.commitLimit = commitLimit;
        this.reserveLimit = reserveLimit;
        WhiteBox wb = WhiteBox.getWhiteBox();
        context = wb.createMetaspaceTestContext(commitLimit, reserveLimit);
        if (context == 0) {
            throw new RuntimeException("Failed to create context");
        }
    }

    // no limits
    public MetaspaceTestContext() {
        this(0, 0);
    }

    public void destroy() {
        if (context != 0) {
            WhiteBox wb = WhiteBox.getWhiteBox();
            wb.destroyMetaspaceTestContext(context);
            context = 0;
        }
    }

    public void purge() {
        if (context != 0) {
            WhiteBox wb = WhiteBox.getWhiteBox();
            wb.purgeMetaspaceTestContext(context);
        }
    }

    public MetaspaceTestArena createArena(boolean is_micro, long ceiling) {
        MetaspaceTestArena arena = null;
        if (context != 0) {
            WhiteBox wb = WhiteBox.getWhiteBox();
            long arena0 = wb.createArenaInTestContext(context, is_micro);
            if (arena0 == 0) {
                throw new RuntimeException("Failed to create arena");
            }
            numArenasCreated++;
            arena = new MetaspaceTestArena(arena0, ceiling);
            arenaList.add(arena);
        }
        return arena;
    }

    public void destroyArena(MetaspaceTestArena a) {
        if (context != 0) {
            if (a.isLive()) {
                WhiteBox wb = WhiteBox.getWhiteBox();
                wb.destroyMetaspaceTestArena(a.arena);
                numArenasDestroyed++;
            }
            arenaList.remove(a);
        }
    }

    public long committedBytes() {
        long l = 0;
        if (context != 0) {
            WhiteBox wb = WhiteBox.getWhiteBox();
            l = wb.getTotalCommittedBytesInMetaspaceTestContext(context);
        }
        return l;
    }

    public long usedBytes() {
        long l = 0;
        if (context != 0) {
            WhiteBox wb = WhiteBox.getWhiteBox();
            l = wb.getTotalUsedBytesInMetaspaceTestContext(context);
        }
        return l;
    }

    public int numLiveArenas() {
        return arenaList.size();
    }

    public void updateTotals() {
        allocatedBytes = deallocatedBytes = numAllocated = numDeallocated = 0;
        for (MetaspaceTestArena a : arenaList) {
            allocatedBytes += a.allocatedBytes;
            deallocatedBytes += a.deallocatedBytes;
            numAllocated += a.numAllocated;
            numDeallocated += a.numDeallocated;
            allocationFailures += a.numAllocationFailures;
        }
    }

    public void printToTTY() {
        if (context != 0) {
            WhiteBox wb = WhiteBox.getWhiteBox();
            wb.printMetaspaceTestContext(context);
        }
    }

    /**
     * Given usage and some context information for current live arenas, do a heuristic about whether the
     * Usage seems right for this case.
     */
    public void checkStatistics() {

        // Note:
        // Estimating Used and Committed is fuzzy, and we only have limited information here
        // (we know the current state, but not the history, which determines fragmentation and
        //  freelist occupancy).
        //
        // We do not want test which constantly generate false positives, so these checks are
        // somewhat loose and only meant to check for clear outliers, e.g. leaks.

        ///// used /////

        updateTotals();

        long usageMeasured = usedBytes();
        long committedMeasured = committedBytes();

        System.out.println("context used bytes " + usageMeasured + ", committed bytes " + committedMeasured
                + ".");

        if (usageMeasured > committedMeasured) {
            throw new RuntimeException("Weirdness.");
        }

        if (deallocatedBytes > allocatedBytes) {
            throw new RuntimeException("Weirdness.");
        }

        // If no arenas are alive, usage should be zero and committed too
        if (numLiveArenas() == 0) {
            if (usageMeasured > 0) {
                throw new RuntimeException("Usage > 0, expected 0");
            }
            if (committedMeasured > 0) {
                throw new RuntimeException("Committed > 0, expected 0");
            }
        }

        long expectedMinUsage = allocatedBytes - deallocatedBytes;

        if (usageMeasured < expectedMinUsage) {
            throw new RuntimeException("Usage too low: " + usageMeasured + " expected at least " + expectedMinUsage);
        }

        long expectedMaxUsage = allocatedBytes;

        // This is necessary a bit fuzzy, since Metaspace usage consists of:
        // - whatever we allocated
        // - deallocated blocks in fbl
        // - remains of retired chunks in fbl
        // - overhead per allocation (padding for alignment)

        // Overhead per allocation (see metaspaceArena.cpp, get_raw_allocation_word_size() )
        // Any allocation is 3 words least
        expectedMaxUsage += (numAllocated * 3 * Settings.WORD_SIZE);

        // Lets add a overhead per arena. Each arena carries a free block list containing
        // deallocated/retired blocks. We do not know how much. In general, the free block list should not
        // accumulate a lot of memory but be drained in the course of allocating memory from the arena.
        long overheadPerArena = 1024 * 1024 * numLiveArenas() * Settings.WORD_SIZE;
        expectedMaxUsage += overheadPerArena;

        if (expectedMaxUsage < usageMeasured) {
            throw new RuntimeException("Usage seems high: " + usageMeasured + " expected at most " + expectedMaxUsage);
        }

        ///// Committed //////

        if (committedMeasured < expectedMinUsage) {
            throw new RuntimeException("Usage too low: " + usageMeasured + " expected at least " + expectedMinUsage);
        }

        // Max committed:
        // This is difficult to estimate, so just a rough guess.
        //
        // Committed space depends on:
        // 1) Usage (how much we allocated + overhead per allocation + free block list content)
        // 2) free space in used chunks
        // 3) committed chunks in freelist.
        //
        // Having only live usage numbers without history, (2) and (3) can only be roughly estimated. Since these
        // are stress tests,
        //
        long expectedMaxCommitted = usageMeasured;
        expectedMaxCommitted += Settings.ROOT_CHUNK_WORD_SIZE * Settings.WORD_SIZE;
        expectedMaxCommitted *= 10.0;

        if (committedMeasured > expectedMaxCommitted) {
            throw new RuntimeException("Committed seems high: " + committedMeasured + " expected at most " + expectedMaxCommitted);
        }

    }

    @java.lang.Override
    public java.lang.String toString() {
        return "MetaspaceTestContext{" +
                "context=" + context +
                ", commitLimit=" + commitLimit +
                ", reserveLimit=" + reserveLimit +
                ", numArenasCreated=" + numArenasCreated +
                ", numArenasDestroyed=" + numArenasDestroyed +
                ", numLiveArenas=" + numLiveArenas() +
                ", allocatedBytes=" + allocatedBytes +
                ", numAllocated=" + numAllocated +
                ", deallocatedBytes=" + deallocatedBytes +
                ", numDeallocated=" + numDeallocated +
                ", allocationFailures=" + allocationFailures +
                '}';
    }
}
