/*
 * 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 java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.TreeSet;

import jdk.test.lib.apps.LingeredApp;
import jdk.test.lib.JDKToolLauncher;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.SA.SATestUtils;

/**
 * @test
 * @bug 8214226 8243500
 * @requires vm.hasSA
 * @requires os.arch=="amd64" | os.arch=="x86_64"
 * @requires os.family=="windows" | os.family == "linux" | os.family == "mac"
 * @requires vm.flagless
 * @library /test/lib
 * @run driver TestJhsdbJstackLineNumbers
 */

/*
 * This test makes sure that SA gets the most accurate value for the line number of
 * the current (topmost) frame. Many SA ports just rely on frame->bcp, but it is
 * usually out of date since the current BCP is cached in a register and only flushed
 * to frame->bcp when the register is needed for something else. Therefore SA ports
 * need to fetch the register that the BCP is stored in and see if it is valid,
 * and only defer to frame->bcp if it is not valid.
 *
 * The test works by spawning a process that sits in a 10 line loop in the busywork() method,
 * all while the main test does repeated jstacks on the process. The expectation is
 * that at least 4 of the lines in the busywork() loop will eventually show up in at
 * least one of the jstack runs.
 */

class LingeredAppWithBusyWork extends LingeredApp {
    static volatile boolean stop = false;

    private static int busywork(int[] x) {
        int i = 0;
        while (!stop) {
            i = x[0];
            i += x[1];
            i += x[2];
            i += x[3];
            i += x[4];
            i += x[5];
            i += x[6];
            i += x[7];
        }
        return i;
    }

    public static void main(String... args) {
        Thread t = new Thread(() -> {
            busywork(new int[]{0,1,2,3,4,5,6,7});
        });

        try {
            t.setName("BusyWorkThread");
            t.start();
            LingeredApp.main(args);
            stop = true;
            t.join();
        } catch (InterruptedException e) {
        }
    }
}

public class TestJhsdbJstackLineNumbers {
    // This is the number of lines in the busywork main loop
    static final int TOTAL_BUSYWORK_LOOP_LINES = 10;
    // The minimum number of lines that we must see at some point in the jstack output.
    // There's always a chance we could see fewer, but the chances are so low that
    // it is unlikely to ever happen. We can always decrease the odds by lowering
    // the required number of lines or increasing the number of jstack runs.
    static final int MIN_BUSYWORK_LOOP_LINES = 4;

    static final int MAX_NUMBER_OF_JSTACK_RUNS = 25;

    private static OutputAnalyzer runJstack(String... toolArgs) throws Exception {
        JDKToolLauncher launcher = JDKToolLauncher.createUsingTestJDK("jhsdb");
        launcher.addToolArg("jstack");
        if (toolArgs != null) {
            for (String toolArg : toolArgs) {
                launcher.addToolArg(toolArg);
            }
        }

        ProcessBuilder processBuilder = SATestUtils.createProcessBuilder(launcher);
        System.out.println(processBuilder.command().stream().collect(Collectors.joining(" ")));
        OutputAnalyzer output = ProcessTools.executeProcess(processBuilder);

        return output;
    }

    public static void runTest(long pid) throws Exception {
        // Keep running jstack until the target app is in the "busywork" method.
        String output;
        int maxRetries = 5;
        do {
            if (maxRetries-- == 0) {
                throw new RuntimeException("Failed: LingeredAppWithBusyWork never entered busywork() method.");
            }
            OutputAnalyzer jstackOut = runJstack("--pid", Long.toString(pid));
            output = jstackOut.getOutput();
            System.out.println(output);
        } while (!output.contains("busywork"));

        // This is for tracking all the line numbers in busywork() that we've seen.
        // Since it is a TreeSet, it will always be sorted and have no duplicates.
        TreeSet<Integer> lineNumbersSeen = new TreeSet<Integer>();

        // Keep running jstack until we see a sufficient number of different line
        // numbers in the busywork() loop.
        for (int x = 0; x < MAX_NUMBER_OF_JSTACK_RUNS; x++) {
            OutputAnalyzer jstackOut = runJstack("--pid", Long.toString(pid));
            output = jstackOut.getOutput();
            // The stack dump will have a line that looks like:
            //   - LingeredAppWithBusyWork.busywork(int[]) @bci=32, line=74 (Interpreted frame)
            // We want to match on the line number, "74" in this example. We also match on the
            // full line just so we can print it out.
            Pattern LINE_PATTERN = Pattern.compile(
                ".+(- LingeredAppWithBusyWork.busywork\\(int\\[\\]\\) \\@bci\\=[0-9]+, line\\=([0-9]+) \\(Interpreted frame\\)).+", Pattern.DOTALL);
            Matcher matcher = LINE_PATTERN.matcher(output);
            if (matcher.matches()) {
                System.out.println(matcher.group(1)); // print matching stack trace line
                int lineNum = Integer.valueOf(matcher.group(2)); // get matching line number
                lineNumbersSeen.add(lineNum);
                if (lineNumbersSeen.size() == MIN_BUSYWORK_LOOP_LINES) {
                    // We're done!
                    System.out.println("Found needed line numbers after " + (x+1) + " iterations");
                    break;
                }
            } else {
                System.out.println("failed to match");
                System.out.println(output);
                continue; // Keep trying. This can happen on rare occasions when the stack cannot be determined.
            }
        }
        System.out.println("Found Line Numbers: " + lineNumbersSeen);

        // Make sure we saw the minimum required number of lines in busywork().
        if (lineNumbersSeen.size() < MIN_BUSYWORK_LOOP_LINES) {
            throw new RuntimeException("Failed: Didn't find enough line numbers: " + lineNumbersSeen);
        }

        // Make sure the distance between the lowest and highest line numbers seen
        // is not more than the number of lines in the busywork() loop.
        if (lineNumbersSeen.last() - lineNumbersSeen.first() > TOTAL_BUSYWORK_LOOP_LINES) {
            throw new RuntimeException("Failed: lowest and highest line numbers are too far apart: " + lineNumbersSeen);
        }

    }

    public static void main(String... args) throws Exception {
        SATestUtils.skipIfCannotAttach(); // throws SkippedException if attach not expected to work.

        LingeredApp theApp = null;
        try {
            // Launch the LingeredAppWithBusyWork process with the busywork() loop
            theApp = new LingeredAppWithBusyWork();
            LingeredApp.startAppExactJvmOpts(theApp, "-Xint");
            System.out.println("Started LingeredApp with pid " + theApp.getPid());

            runTest(theApp.getPid());
        } finally {
            LingeredApp.stopApp(theApp);
            System.out.println("LingeredAppWithBusyWork finished");
        }
    }
}
