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

/*
 * @test
 * @bug     8036823
 * @bug     8046287
 * @summary Creates two threads contending for the same lock and checks
 *      whether jstack reports "locked" by more than one thread.
 *
 * @library /testlibrary
 * @run main/othervm TestThreadDumpMonitorContention
 */

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.oracle.java.testlibrary.*;

public class TestThreadDumpMonitorContention {
    // jstack tends to be closely bound to the VM that we are running
    // so use getTestJDKTool() instead of getCompileJDKTool() or even
    // getJDKTool() which can fall back to "compile.jdk".
    final static String JSTACK = JDKToolFinder.getTestJDKTool("jstack");
    final static String PID = getPid();

    // looking for header lines with these patterns:
    // "ContendingThread-1" #19 prio=5 os_prio=64 tid=0x000000000079c000 nid=0x23 runnable [0xffff80ffb8b87000]
    // "ContendingThread-2" #21 prio=5 os_prio=64 tid=0x0000000000780000 nid=0x2f waiting for monitor entry [0xfffffd7fc1111000]
    // "ContendingThread-2" #24 prio=5 os_prio=64 tid=0x0000000000ec8800 nid=0x31 waiting on condition [0xfffffd7bbfffe000]
    final static Pattern HEADER_PREFIX_PATTERN = Pattern.compile(
        "^\"ContendingThread-.*");
    final static Pattern HEADER_WAITING_PATTERN1 = Pattern.compile(
        "^\"ContendingThread-.* waiting for monitor entry .*");
    final static Pattern HEADER_WAITING_PATTERN2 = Pattern.compile(
        "^\"ContendingThread-.* waiting on condition .*");
    final static Pattern HEADER_RUNNABLE_PATTERN = Pattern.compile(
        "^\"ContendingThread-.* runnable .*");

    // looking for thread state lines with these patterns:
    // java.lang.Thread.State: RUNNABLE
    // java.lang.Thread.State: BLOCKED (on object monitor)
    final static Pattern THREAD_STATE_PREFIX_PATTERN = Pattern.compile(
        " *java\\.lang\\.Thread\\.State: .*");
    final static Pattern THREAD_STATE_BLOCKED_PATTERN = Pattern.compile(
        " *java\\.lang\\.Thread\\.State: BLOCKED \\(on object monitor\\)");
    final static Pattern THREAD_STATE_RUNNABLE_PATTERN = Pattern.compile(
        " *java\\.lang\\.Thread\\.State: RUNNABLE");

    // looking for duplicates of this pattern:
    // - locked <0x000000076ac59e20> (a TestThreadDumpMonitorContention$1)
    final static Pattern LOCK_PATTERN = Pattern.compile(
        ".* locked \\<.*\\(a TestThreadDumpMonitorContention.*");

    // sanity checking header and thread state lines associated
    // with this pattern:
    // - waiting to lock <0x000000076ac59e20> (a TestThreadDumpMonitorContention$1)
    final static Pattern WAITING_PATTERN = Pattern.compile(
        ".* waiting to lock \\<.*\\(a TestThreadDumpMonitorContention.*");

    final static Object barrier = new Object();
    volatile static boolean done = false;

    static int barrier_cnt = 0;
    static int blank_line_match_cnt = 0;
    static int error_cnt = 0;
    static boolean have_header_line = false;
    static boolean have_thread_state_line = false;
    static String header_line = null;
    static int header_prefix_match_cnt = 0;
    static int locked_line_match_cnt = 0;
    static String[] locked_match_list = new String[2];
    static int n_samples = 15;
    static int sum_both_running_cnt = 0;
    static int sum_both_waiting_cnt = 0;
    static int sum_contended_cnt = 0;
    static int sum_locked_hdr_runnable_cnt = 0;
    static int sum_locked_hdr_waiting1_cnt = 0;
    static int sum_locked_hdr_waiting2_cnt = 0;
    static int sum_locked_thr_state_blocked_cnt = 0;
    static int sum_locked_thr_state_runnable_cnt = 0;
    static int sum_one_waiting_cnt = 0;
    static int sum_uncontended_cnt = 0;
    static int sum_waiting_hdr_waiting1_cnt = 0;
    static int sum_waiting_thr_state_blocked_cnt = 0;
    static String thread_state_line = null;
    static boolean verbose = false;
    static int waiting_line_match_cnt = 0;

    public static void main(String[] args) throws Exception {
        if (args.length != 0) {
            int arg_i = 0;
            if (args[arg_i].equals("-v")) {
                verbose = true;
                arg_i++;
            }

            try {
                n_samples = Integer.parseInt(args[arg_i]);
            } catch (NumberFormatException nfe) {
                System.err.println(nfe);
                usage();
            }
        }

        Runnable runnable = new Runnable() {
            public void run() {
                synchronized (barrier) {
                    // let the main thread know we're running
                    barrier_cnt++;
                    barrier.notify();
                }
                while (!done) {
                    synchronized (this) { }
                }
            }
        };
        Thread[] thread_list = new Thread[2];
        thread_list[0] = new Thread(runnable, "ContendingThread-1");
        thread_list[1] = new Thread(runnable, "ContendingThread-2");
        synchronized (barrier) {
            thread_list[0].start();
            thread_list[1].start();

            // Wait until the contending threads are running so that
            // we don't sample any thread init states.
            while (barrier_cnt < 2) {
                barrier.wait();
            }
        }

        doSamples();

        done = true;

        thread_list[0].join();
        thread_list[1].join();

        if (error_cnt == 0) {
            System.out.println("Test PASSED.");
        } else {
            System.out.println("Test FAILED.");
            throw new AssertionError("error_cnt=" + error_cnt);
        }
    }

    // Reached a blank line which is the end of the
    // stack trace without matching either LOCK_PATTERN
    // or WAITING_PATTERN. Rare, but it's not an error.
    //
    // Example:
    // "ContendingThread-1" #21 prio=5 os_prio=64 tid=0x00000000007b9000 nid=0x2f runnable [0xfffffd7fc1111000]
    //    java.lang.Thread.State: RUNNABLE
    //         at TestThreadDumpMonitorContention$1.run(TestThreadDumpMonitorContention.java:140)
    //         at java.lang.Thread.run(Thread.java:745)
    //
    static boolean checkBlankLine(String line) {
        if (line.length() == 0) {
            blank_line_match_cnt++;
            have_header_line = false;
            have_thread_state_line = false;
            return true;
        }

        return false;
    }

    // Process the locked line here if we found one.
    //
    // Example 1:
    // "ContendingThread-1" #21 prio=5 os_prio=64 tid=0x00000000007b9000 nid=0x2f runnable [0xfffffd7fc1111000]
    //    java.lang.Thread.State: RUNNABLE
    //         at TestThreadDumpMonitorContention$1.run(TestThreadDumpMonitorContention.java:140)
    //         - locked <0xfffffd7e6a2912f8> (a TestThreadDumpMonitorContention$1)
    //         at java.lang.Thread.run(Thread.java:745)
    //
    // Example 2:
    // "ContendingThread-1" #21 prio=5 os_prio=64 tid=0x00000000007b9000 nid=0x2f waiting for monitor entry [0xfffffd7fc1111000]
    //    java.lang.Thread.State: BLOCKED (on object monitor)
    //         at TestThreadDumpMonitorContention$1.run(TestThreadDumpMonitorContention.java:140)
    //         - locked <0xfffffd7e6a2912f8> (a TestThreadDumpMonitorContention$1)
    //         at java.lang.Thread.run(Thread.java:745)
    //
    // Example 3:
    // "ContendingThread-2" #24 prio=5 os_prio=64 tid=0x0000000000ec8800 nid=0x31 waiting on condition [0xfffffd7bbfffe000]
    //    java.lang.Thread.State: RUNNABLE
    //    JavaThread state: _thread_blocked
    // Thread: 0x0000000000ec8800  [0x31] State: _at_safepoint _has_called_back 0 _at_poll_safepoint 0
    //    JavaThread state: _thread_blocked
    //         at TestThreadDumpMonitorContention$1.run(TestThreadDumpMonitorContention.java:140)
    //         - locked <0xfffffd7e6d03eb28> (a TestThreadDumpMonitorContention$1)
    //         at java.lang.Thread.run(Thread.java:745)
    //
    static boolean checkLockedLine(String line) {
        Matcher matcher = LOCK_PATTERN.matcher(line);
        if (matcher.matches()) {
            if (verbose) {
                System.out.println("locked_line='" + line + "'");
            }
            locked_match_list[locked_line_match_cnt] = new String(line);
            locked_line_match_cnt++;

            matcher = HEADER_RUNNABLE_PATTERN.matcher(header_line);
            if (matcher.matches()) {
                sum_locked_hdr_runnable_cnt++;
            } else {
                // It's strange, but a locked line can also
                // match the HEADER_WAITING_PATTERN{1,2}.
                matcher = HEADER_WAITING_PATTERN1.matcher(header_line);
                if (matcher.matches()) {
                    sum_locked_hdr_waiting1_cnt++;
                } else {
                    matcher = HEADER_WAITING_PATTERN2.matcher(header_line);
                    if (matcher.matches()) {
                        sum_locked_hdr_waiting2_cnt++;
                    } else {
                        System.err.println();
                        System.err.println("ERROR: header line does " +
                            "not match runnable or waiting patterns.");
                        System.err.println("ERROR: header_line='" +
                            header_line + "'");
                        System.err.println("ERROR: locked_line='" + line +
                            "'");
                        error_cnt++;
                    }
                }
            }

            matcher = THREAD_STATE_RUNNABLE_PATTERN.matcher(thread_state_line);
            if (matcher.matches()) {
                sum_locked_thr_state_runnable_cnt++;
            } else {
                // It's strange, but a locked line can also
                // match the THREAD_STATE_BLOCKED_PATTERN.
                matcher = THREAD_STATE_BLOCKED_PATTERN.matcher(
                              thread_state_line);
                if (matcher.matches()) {
                    sum_locked_thr_state_blocked_cnt++;
                } else {
                    System.err.println();
                    System.err.println("ERROR: thread state line does not " +
                        "match runnable or waiting patterns.");
                    System.err.println("ERROR: " + "thread_state_line='" +
                        thread_state_line + "'");
                    System.err.println("ERROR: locked_line='" + line + "'");
                    error_cnt++;
                }
            }

            // Have everything we need from this thread stack
            // that matches the LOCK_PATTERN.
            have_header_line = false;
            have_thread_state_line = false;
            return true;
        }

        return false;
    }

    // Process the waiting line here if we found one.
    //
    // Example:
    // "ContendingThread-2" #22 prio=5 os_prio=64 tid=0x00000000007b9800 nid=0x30 waiting for monitor entry [0xfffffd7fc1010000]
    //    java.lang.Thread.State: BLOCKED (on object monitor)
    //         at TestThreadDumpMonitorContention$1.run(TestThreadDumpMonitorContention.java:140)
    //         - waiting to lock <0xfffffd7e6a2912f8> (a TestThreadDumpMonitorContention$1)
    //         at java.lang.Thread.run(Thread.java:745)
    //
    static boolean checkWaitingLine(String line) {
        Matcher matcher = WAITING_PATTERN.matcher(line);
        if (matcher.matches()) {
            waiting_line_match_cnt++;
            if (verbose) {
                System.out.println("waiting_line='" + line + "'");
            }

            matcher = HEADER_WAITING_PATTERN1.matcher(header_line);
            if (matcher.matches()) {
                sum_waiting_hdr_waiting1_cnt++;
            } else {
                System.err.println();
                System.err.println("ERROR: header line does " +
                    "not match a waiting pattern.");
                System.err.println("ERROR: header_line='" + header_line + "'");
                System.err.println("ERROR: waiting_line='" + line + "'");
                error_cnt++;
            }

            matcher = THREAD_STATE_BLOCKED_PATTERN.matcher(thread_state_line);
            if (matcher.matches()) {
                sum_waiting_thr_state_blocked_cnt++;
            } else {
                System.err.println();
                System.err.println("ERROR: thread state line " +
                    "does not match a waiting pattern.");
                System.err.println("ERROR: thread_state_line='" +
                    thread_state_line + "'");
                System.err.println("ERROR: waiting_line='" + line + "'");
                error_cnt++;
            }

            // Have everything we need from this thread stack
            // that matches the WAITING_PATTERN.
            have_header_line = false;
            have_thread_state_line = false;
            return true;
        }

        return false;
    }

    static void doSamples() throws Exception {
        for (int count = 0; count < n_samples; count++) {
            blank_line_match_cnt = 0;
            header_prefix_match_cnt = 0;
            locked_line_match_cnt = 0;
            waiting_line_match_cnt = 0;
            // verbose mode or an error has a lot of output so add more space
            if (verbose || error_cnt > 0) System.out.println();
            System.out.println("Sample #" + count);

            // We don't use the ProcessTools, OutputBuffer or
            // OutputAnalyzer classes from the testlibrary because
            // we have a complicated multi-line parse to perform
            // on a narrow subset of the JSTACK output.
            //
            // - we only care about stack traces that match
            //   HEADER_PREFIX_PATTERN; only two should match
            // - we care about at most three lines from each stack trace
            // - if both stack traces match LOCKED_PATTERN, then that's
            //   a failure and we report it
            // - for a stack trace that matches LOCKED_PATTERN, we verify:
            //   - the header line matches HEADER_RUNNABLE_PATTERN
            //     or HEADER_WAITING_PATTERN{1,2}
            //   - the thread state line matches THREAD_STATE_BLOCKED_PATTERN
            //     or THREAD_STATE_RUNNABLE_PATTERN
            //   - we report any mismatches as failures
            // - for a stack trace that matches WAITING_PATTERN, we verify:
            //   - the header line matches HEADER_WAITING_PATTERN1
            //   - the thread state line matches THREAD_STATE_BLOCKED_PATTERN
            //   - we report any mismatches as failures
            // - the stack traces that match HEADER_PREFIX_PATTERN may
            //   not match either LOCKED_PATTERN or WAITING_PATTERN
            //   because we might observe the thread outside of
            //   monitor operations; this is not considered a failure
            //
            // When we do observe LOCKED_PATTERN or WAITING_PATTERN,
            // then we are checking the header and thread state patterns
            // that occurred earlier in the current stack trace that
            // matched HEADER_PREFIX_PATTERN. We don't use data from
            // stack traces that don't match HEADER_PREFIX_PATTERN and
            // we don't mix data between the two stack traces that do
            // match HEADER_PREFIX_PATTERN.
            //
            Process process = new ProcessBuilder(JSTACK, PID)
                .redirectErrorStream(true).start();

            BufferedReader reader = new BufferedReader(new InputStreamReader(
                                        process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                Matcher matcher = null;

                // process the header line here
                if (!have_header_line) {
                    matcher = HEADER_PREFIX_PATTERN.matcher(line);
                    if (matcher.matches()) {
                        header_prefix_match_cnt++;
                        if (verbose) {
                            System.out.println();
                            System.out.println("header='" + line + "'");
                        }
                        header_line = new String(line);
                        have_header_line = true;
                        continue;
                    }
                    continue;  // skip until have a header line
                }

                // process the thread state line here
                if (!have_thread_state_line) {
                    matcher = THREAD_STATE_PREFIX_PATTERN.matcher(line);
                    if (matcher.matches()) {
                        if (verbose) {
                            System.out.println("thread_state='" + line + "'");
                        }
                        thread_state_line = new String(line);
                        have_thread_state_line = true;
                        continue;
                    }
                    continue;  // skip until we have a thread state line
                }

                // process the locked line here if we find one
                if (checkLockedLine(line)) {
                    continue;
                }

                // process the waiting line here if we find one
                if (checkWaitingLine(line)) {
                    continue;
                }

                // process the blank line here if we find one
                if (checkBlankLine(line)) {
                    continue;
                }
            }
            process.waitFor();

            if (header_prefix_match_cnt != 2) {
                System.err.println();
                System.err.println("ERROR: should match exactly two headers.");
                System.err.println("ERROR: header_prefix_match_cnt=" +
                    header_prefix_match_cnt);
                error_cnt++;
            }

            if (locked_line_match_cnt == 2) {
                if (locked_match_list[0].equals(locked_match_list[1])) {
                    System.err.println();
                    System.err.println("ERROR: matching lock lines:");
                    System.err.println("ERROR: line[0]'" +
                        locked_match_list[0] + "'");
                    System.err.println("ERROR: line[1]'" +
                        locked_match_list[1] + "'");
                    error_cnt++;
                }
            }

            if (locked_line_match_cnt == 1) {
                // one thread has the lock
                if (waiting_line_match_cnt == 1) {
                    // and the other contended for it
                    sum_contended_cnt++;
                } else {
                    // and the other is just running
                    sum_uncontended_cnt++;
                }
            } else if (waiting_line_match_cnt == 1) {
                // one thread is waiting
                sum_one_waiting_cnt++;
            } else if (waiting_line_match_cnt == 2) {
                // both threads are waiting
                sum_both_waiting_cnt++;
            } else {
                // both threads are running
                sum_both_running_cnt++;
            }

            // slight delay between jstack launches
            Thread.sleep(500);
        }

        if (error_cnt != 0) {
            // skip summary info since there were errors
            return;
        }

        System.out.println("INFO: Summary for all samples:");
        System.out.println("INFO: both_running_cnt=" + sum_both_running_cnt);
        System.out.println("INFO: both_waiting_cnt=" + sum_both_waiting_cnt);
        System.out.println("INFO: contended_cnt=" + sum_contended_cnt);
        System.out.println("INFO: one_waiting_cnt=" + sum_one_waiting_cnt);
        System.out.println("INFO: uncontended_cnt=" + sum_uncontended_cnt);
        System.out.println("INFO: locked_hdr_runnable_cnt=" +
            sum_locked_hdr_runnable_cnt);
        System.out.println("INFO: locked_hdr_waiting1_cnt=" +
            sum_locked_hdr_waiting1_cnt);
        System.out.println("INFO: locked_hdr_waiting2_cnt=" +
            sum_locked_hdr_waiting2_cnt);
        System.out.println("INFO: locked_thr_state_blocked_cnt=" +
            sum_locked_thr_state_blocked_cnt);
        System.out.println("INFO: locked_thr_state_runnable_cnt=" +
            sum_locked_thr_state_runnable_cnt);
        System.out.println("INFO: waiting_hdr_waiting1_cnt=" +
            sum_waiting_hdr_waiting1_cnt);
        System.out.println("INFO: waiting_thr_state_blocked_cnt=" +
            sum_waiting_thr_state_blocked_cnt);

        if (sum_contended_cnt == 0) {
            System.err.println("WARNING: the primary scenario for 8036823" +
                " has not been exercised by this test run.");
        }
    }

    // This helper relies on RuntimeMXBean.getName() returning a string
    // that looks like this: 5436@mt-haku
    //
    // The testlibrary has tryFindJvmPid(), but that uses a separate
    // process which is much more expensive for finding out your own PID.
    //
    static String getPid() {
        RuntimeMXBean runtimebean = ManagementFactory.getRuntimeMXBean();
        String vmname = runtimebean.getName();
        int i = vmname.indexOf('@');
        if (i != -1) {
            vmname = vmname.substring(0, i);
        }
        return vmname;
    }

    static void usage() {
        System.err.println("Usage: " +
            "java TestThreadDumpMonitorContention [-v] [n_samples]");
        System.exit(1);
    }
}
