/*
 * Copyright (c) 2013, 2022, 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.
 */

package metaspace.gc;

import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static metaspace.gc.MetaspaceBaseGC.PAGE_SIZE;

/**
 * Test for metaspace GC
 *
 * <ul>
 * <li>MetaspaceSize
 * </ul>
 * Test checks that the first GC happens when metaspace committed is next to
 * MetaspaceSize value.
 *
 * Based on actual events (JDK 8 GC tuning document)
 *
 * Quating: Java SE 8 HotSpot[tm] Virtual Machine Garbage Collection Tuning
 * <pre>
 * Class metadata is deallocated when the corresponding Java class is unloaded.
 * Java classes are unloaded as a results of garbage collection and garbage
 * collections may be induced in order to unload classes and deallocate class
 * metadata. When the space used for class metadata reaches a certain level
 * (call it a high-water mark), a garbage collection is induced.
 *
 * The flag MetaspaceSize can be set higher to avoid early garbage collections
 * induced for class metadata. The amount of class metadata allocated for
 * an application is application dependent and general guidelines do not
 * exist for the selection of MetaspaceSize. The default size of MetaspaceSize
 * is platform dependent and ranges from 12 MB to about 20 MB.
 * </pre>
 */
public class FirstGCTest extends MetaspaceBaseGC {
     /**
      * Current amount of the used metaspace
      */
     protected long used = 0;

     /**
      * Current amount of the committed metaspace
      */
     protected long committed = 0;

     /**
      * Previous amount of the used metaspace
      */
     protected long p_used = 0 ;

     /**
      * Previous amount of the committed metaspace
      */
     protected long p_committed = 0;

    public static void main(String... args) {
        new FirstGCTest().run(args);
    }

    // value given in -XX:metaspaceSize=<value>
    private long metaspaceSize = -1;


    @Override
    protected void parseArgs(String[] args) {
        final String XXSize = "-XX:MetaspaceSize=";
        for (String va: vmArgs) {
            if (va.startsWith(XXSize)) {
                metaspaceSize = parseValue(va.substring(XXSize.length()));
            }
        }
    }

    @Override
    protected String getPoolName() {
        return "Metaspace";
    }

    /**
     * Check for the first GC moment.
     *
     * Eats memory until GC is invoked (amount of used metaspace became less);
     * Checks that committed memory is close to MemaspaceSize.
     * Eats memory until the second GC to check min/max ratio options have effect.
     */
    @Override
    public void doCheck() {
        int gcCount = super.getMetaspaceGCCount();
        if (gcCount == 0) {
            // gc hasn't happened yet. Start loading classes.
            boolean gcHappened = this.eatMemoryUntilGC(50000);
            if (!gcHappened) {
                throw new Fault("GC hasn't happened");
            }
            System.out.println("% GC: " + super.lastGCLogLine());
            System.out.println("%   used     : " + p_used + " --> " + used);
            System.out.println("%   committed: " + p_committed + " --> " + committed);
            checkCommitted(p_committed);
        } else {
            // everything has happened before
            checkCommitted(detectCommittedFromGCLog());
        }
    }

    /**
     * Check that committed amount is close to expected value (MetaspaceSize)
     *
     * @param committedAmount - value to check
     */
    void checkCommitted(long committedAmount) {
        if (metaspaceSize > 0) {
            // -XX:MetaspaceSize is given
            if (Math.abs((int) (metaspaceSize - committedAmount)) < PAGE_SIZE) {
                System.out.println("% GC happened at the right moment");
                return;
            }
            if (!isMetaspaceGC()) {
                System.out.println("% GC wasn't induced by metaspace, cannot check the moment :(");
                return;
            }
            System.err.println("%## GC happened at the wrong moment, "
                    + "the amount of committed space significantly differs "
                    + "from the expected amount");
            System.err.println("%## Real    : " + committedAmount);
            System.err.println("%## Expected: " + metaspaceSize);
            throw new Fault("GC happened at the wrong moment");
        } else {
            // -XX:MetaspaceSize is not given, check for default values
            if (11_500_000 < committedAmount && committedAmount < 22_500_000) {
                System.out.println("% GC happened when the committed amout was from 12 MB to about 20 MB.");
                return;
            }
            if (!isMetaspaceGC()) {
                System.out.println("% GC wasn't induced by metaspace, this is excuse");
                return;
            }
            System.err.println("%## GC happened at the wrong moment, "
                    + "the amount of committed space was expected from 12 MB to about 20 MB");
            System.err.println("%## Real    : " + committedAmount);
            throw new Fault("It was the wrong moment when GC happened");
        }
    }

    /**
     * Load new classes without keeping references to them trying to provoke GC.
     * Stops if GC is detected, or number of attempts exceeds the given limit.
     *
     * @param times limit of attempts to provoke GC
     * @return true if GC has happened, false if limit has exceeded.
     */
    protected boolean eatMemoryUntilGC(int times) {
        System.out.println("%%%% Loading classes");
        System.out.println("% iter#  :   used  : commited");
        System.out.println("..............................");
        for (int i = 1; i < times; i++) {
            loadNewClasses(1, false);
            if (i % 1000 == 0) {
                printMemoryUsage("%  " + i + "  ");
            }
            p_used      = used;
            p_committed = committed;
            used = getUsed();
            committed = getCommitted();

            if (used < p_used) {
                return true;
            }
        }
        return false;
    }

    /**
     * If the first full GC has already happened we will try to detect
     * the committed amount from the gc.log file.
     *
     * @return committed amount detected
     * @throws Fault if failed to detect.
     */
    protected long detectCommittedFromGCLog() {
        // parse gc.log to extract the committed value from string like:
        //  Metaspace       used 10133K, capacity 10190K, committed 10240K, reserved 10240Kl
        System.out.println("%%%% Parsing gc log to detect the moment of the first GC");
        String format = ".*Metaspace.* used .*, capacity .*, committed (\\d+)([KMGkmg]), reserved .*";
        Pattern p = Pattern.compile(format);
        try {
            for (String line: readGCLog()) {
                Matcher m = p.matcher(line);
                if (m.matches()) {
                    int amount = Integer.parseInt(m.group(1));
                    int multi = 1;
                    switch (m.group(2).toLowerCase()) {
                        case "k": multi = 1024; break;
                        case "m": multi = 1024*1024; break;
                        case "g": multi = 1024*1024*1024; break;
                    }
                    long value = amount * multi;
                    System.out.println("% Committed detected: " + value);
                    return value;
                }
            }
        } catch (IOException e) {
            throw new Fault("Cannot read from the GC log");
        }
        System.out.println("% String that matches pattern '" + format + "' not found in the GC log file.");
        throw new Fault("Unable to detect the moment of GC from log file");
    }

}