/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import dalvik.system.VMRuntime;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.LockSupport;

// Run on host with:
//   javac ThreadTest.java && java ThreadStress && rm *.class
// Through run-test:
//   test/run-test {run-test-args} 004-ThreadStress [Main {ThreadStress-args}]
//   (It is important to pass Main if you want to give parameters...)
//
// ThreadStress command line parameters:
//    -n X .............. number of threads
//    -d X .............. number of daemon threads
//    -o X .............. number of overall operations
//    -t X .............. number of operations per thread
//    -p X .............. number of permits granted by semaphore
//    --dumpmap ......... print the frequency map
//    --locks-only ...... select a pre-set frequency map with lock-related operations only
//    --allocs-only ..... select a pre-set frequency map with allocation-related operations only
//    -oom:X ............ frequency of OOM (double)
//    -sigquit:X ........ frequency of SigQuit (double)
//    -alloc:X .......... frequency of Alloc (double)
//    -largealloc:X ..... frequency of LargeAlloc (double)
//    -nonmovingalloc:X.. frequency of NonMovingAlloc (double)
//    -stacktrace:X ..... frequency of StackTrace (double)
//    -exit:X ........... frequency of Exit (double)
//    -sleep:X .......... frequency of Sleep (double)
//    -wait:X ........... frequency of Wait (double)
//    -timedwait:X ...... frequency of TimedWait (double)
//    -timedpark:X ...... frequency of TimedPark (double)
//    -syncandwork:X .... frequency of SyncAndWork (double)
//    -queuedwait:X ..... frequency of QueuedWait (double)

public class Main implements Runnable {

    public static final boolean DEBUG = false;

    private static abstract class Operation {
        /**
         * Perform the action represented by this operation. Returns true if the thread should
         * continue when executed by a runner (non-daemon) thread.
         */
        public abstract boolean perform();
    }

    private final static class OOM extends Operation {
        private final static int ALLOC_SIZE = 1024;

        @Override
        public boolean perform() {
            try {
                List<byte[]> l = new ArrayList<byte[]>();
                while (true) {
                    l.add(new byte[ALLOC_SIZE]);
                }
            } catch (OutOfMemoryError e) {
            }
            return true;
        }
    }

    private final static class SigQuit extends Operation {
        private final static int sigquit;
        private final static Method kill;
        private final static int pid;

        static {
            int pidTemp = -1;
            int sigquitTemp = -1;
            Method killTemp = null;

            try {
                Class<?> osClass = Class.forName("android.system.Os");
                Method getpid = osClass.getDeclaredMethod("getpid");
                pidTemp = (Integer)getpid.invoke(null);

                Class<?> osConstants = Class.forName("android.system.OsConstants");
                Field sigquitField = osConstants.getDeclaredField("SIGQUIT");
                sigquitTemp = (Integer)sigquitField.get(null);

                killTemp = osClass.getDeclaredMethod("kill", int.class, int.class);
            } catch (Exception e) {
                Main.printThrowable(e);
            }

            pid = pidTemp;
            sigquit = sigquitTemp;
            kill = killTemp;
        }

        @Override
        public boolean perform() {
            try {
                kill.invoke(null, pid, sigquit);
            } catch (OutOfMemoryError e) {
            } catch (Exception e) {
                if (!e.getClass().getName().equals(Main.errnoExceptionName)) {
                    Main.printThrowable(e);
                }
            }
            return true;
        }
    }

    private final static class Alloc extends Operation {
        private final static int ALLOC_SIZE = 1024;  // Needs to be small enough to not be in LOS.
        private final static int ALLOC_COUNT = 1024;

        @Override
        public boolean perform() {
            try {
                List<byte[]> l = new ArrayList<byte[]>();
                for (int i = 0; i < ALLOC_COUNT; i++) {
                    l.add(new byte[ALLOC_SIZE]);
                }
            } catch (OutOfMemoryError e) {
            }
            return true;
        }
    }

    private final static class LargeAlloc extends Operation {
        private final static int PAGE_SIZE = 4096;
        private final static int PAGE_SIZE_MODIFIER = 10;  // Needs to be large enough for LOS.
        private final static int ALLOC_COUNT = 100;

        @Override
        public boolean perform() {
            try {
                List<byte[]> l = new ArrayList<byte[]>();
                for (int i = 0; i < ALLOC_COUNT; i++) {
                    l.add(new byte[PAGE_SIZE_MODIFIER * PAGE_SIZE]);
                }
            } catch (OutOfMemoryError e) {
            }
            return true;
        }
    }

  private final static class NonMovingAlloc extends Operation {
        private final static int ALLOC_SIZE = 1024;  // Needs to be small enough to not be in LOS.
        private final static int ALLOC_COUNT = 1024;
        private final static VMRuntime runtime = VMRuntime.getRuntime();

        @Override
        public boolean perform() {
            try {
                List<byte[]> l = new ArrayList<byte[]>();
                for (int i = 0; i < ALLOC_COUNT; i++) {
                    l.add((byte[]) runtime.newNonMovableArray(byte.class, ALLOC_SIZE));
                }
            } catch (OutOfMemoryError e) {
            }
            return true;
        }
    }


    private final static class StackTrace extends Operation {
        @Override
        public boolean perform() {
            try {
                Thread.currentThread().getStackTrace();
            } catch (OutOfMemoryError e) {
            }
            return true;
        }
    }

    private final static class Exit extends Operation {
        @Override
        public boolean perform() {
            return false;
        }
    }

    private final static class Sleep extends Operation {
        private final static int SLEEP_TIME = 100;

        @Override
        public boolean perform() {
            try {
                Thread.sleep(SLEEP_TIME);
            } catch (InterruptedException ignored) {
            }
            return true;
        }
    }

    private final static class TimedWait extends Operation {
        private final static int SLEEP_TIME = 100;

        private final Object lock;

        public TimedWait(Object lock) {
            this.lock = lock;
        }

        @Override
        public boolean perform() {
            synchronized (lock) {
                try {
                    lock.wait(SLEEP_TIME, 0);
                } catch (InterruptedException ignored) {
                }
            }
            return true;
        }
    }

    private final static class Wait extends Operation {
        private final Object lock;

        public Wait(Object lock) {
            this.lock = lock;
        }

        @Override
        public boolean perform() {
            synchronized (lock) {
                try {
                    lock.wait();
                } catch (InterruptedException ignored) {
                }
            }
            return true;
        }
    }

    private final static class TimedPark extends Operation {
        private final static int SLEEP_TIME = 100;

        public TimedPark() {}

        @Override
        public boolean perform() {
            LockSupport.parkNanos(this, 100*1000000);
            return true;
        }
    }

    private final static class SyncAndWork extends Operation {
        private final Object lock;

        public SyncAndWork(Object lock) {
            this.lock = lock;
        }

        @Override
        public boolean perform() {
            synchronized (lock) {
                try {
                    Thread.sleep((int)(Math.random() * 50 + 50));
                } catch (InterruptedException ignored) {
                }
            }
            return true;
        }
    }

    // An operation requiring the acquisition of a permit from a semaphore
    // for its execution. This operation has been added to exercise
    // java.util.concurrent.locks.AbstractQueuedSynchronizer, used in the
    // implementation of java.util.concurrent.Semaphore. We use the latter,
    // as the former is not supposed to be used directly (see b/63822989).
    private final static class QueuedWait extends Operation {
        private final static int SLEEP_TIME = 100;

        private final Semaphore semaphore;

        public QueuedWait(Semaphore semaphore) {
            this.semaphore = semaphore;
        }

        @Override
        public boolean perform() {
            boolean permitAcquired = false;
            try {
                semaphore.acquire();
                permitAcquired = true;
                Thread.sleep(SLEEP_TIME);
            } catch (OutOfMemoryError ignored) {
              // The call to semaphore.acquire() above may trigger an OOME,
              // despite the care taken doing some warm-up by forcing
              // ahead-of-time initialization of classes used by the Semaphore
              // class (see forceTransitiveClassInitialization below).
              // For instance, one of the code paths executes
              // AbstractQueuedSynchronizer.addWaiter, which allocates an
              // AbstractQueuedSynchronizer$Node (see b/67730573).
              // In that case, just ignore the OOME and continue.
            } catch (InterruptedException ignored) {
            } finally {
                if (permitAcquired) {
                    semaphore.release();
                }
            }
            return true;
        }
    }

    private final static Map<Operation, Double> createDefaultFrequencyMap(Object lock,
            Semaphore semaphore) {
        Map<Operation, Double> frequencyMap = new HashMap<Operation, Double>();
        frequencyMap.put(new OOM(), 0.005);                   //   1/200
        frequencyMap.put(new SigQuit(), 0.095);               //  19/200
        frequencyMap.put(new Alloc(), 0.2);                   //  40/200
        frequencyMap.put(new LargeAlloc(), 0.05);             //  10/200
        frequencyMap.put(new NonMovingAlloc(), 0.025);        //   5/200
        frequencyMap.put(new StackTrace(), 0.1);              //  20/200
        frequencyMap.put(new Exit(), 0.225);                  //  45/200
        frequencyMap.put(new Sleep(), 0.075);                 //  15/200
        frequencyMap.put(new TimedPark(), 0.05);              //  10/200
        frequencyMap.put(new TimedWait(lock), 0.05);          //  10/200
        frequencyMap.put(new Wait(lock), 0.075);              //  15/200
        frequencyMap.put(new QueuedWait(semaphore), 0.05);    //  10/200

        return frequencyMap;
    }

    private final static Map<Operation, Double> createAllocFrequencyMap() {
        Map<Operation, Double> frequencyMap = new HashMap<Operation, Double>();
        frequencyMap.put(new Sleep(), 0.2);                   //  40/200
        frequencyMap.put(new Alloc(), 0.575);                 // 115/200
        frequencyMap.put(new LargeAlloc(), 0.15);             //  30/200
        frequencyMap.put(new NonMovingAlloc(), 0.075);        //  15/200

        return frequencyMap;
    }

    private final static Map<Operation, Double> createLockFrequencyMap(Object lock) {
      Map<Operation, Double> frequencyMap = new HashMap<Operation, Double>();
      frequencyMap.put(new Sleep(), 0.2);                     //  40/200
      frequencyMap.put(new TimedWait(lock), 0.1);             //  20/200
      frequencyMap.put(new Wait(lock), 0.2);                  //  40/200
      frequencyMap.put(new SyncAndWork(lock), 0.4);           //  80/200
      frequencyMap.put(new TimedPark(), 0.1);                 //  20/200

      return frequencyMap;
    }

    public static void main(String[] args) throws Exception {
        System.loadLibrary(args[0]);
        parseAndRun(args);
    }

    private static Map<Operation, Double> updateFrequencyMap(Map<Operation, Double> in,
            Object lock, Semaphore semaphore, String arg) {
        String split[] = arg.split(":");
        if (split.length != 2) {
            throw new IllegalArgumentException("Can't split argument " + arg);
        }
        double d;
        try {
            d = Double.parseDouble(split[1]);
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
        if (d < 0) {
            throw new IllegalArgumentException(arg + ": value must be >= 0.");
        }
        Operation op = null;
        if (split[0].equals("-oom")) {
            op = new OOM();
        } else if (split[0].equals("-sigquit")) {
            op = new SigQuit();
        } else if (split[0].equals("-alloc")) {
            op = new Alloc();
        } else if (split[0].equals("-largealloc")) {
            op = new LargeAlloc();
        } else if (split[0].equals("-nonmovingalloc")) {
            op = new NonMovingAlloc();
        } else if (split[0].equals("-stacktrace")) {
            op = new StackTrace();
        } else if (split[0].equals("-exit")) {
            op = new Exit();
        } else if (split[0].equals("-sleep")) {
            op = new Sleep();
        } else if (split[0].equals("-wait")) {
            op = new Wait(lock);
        } else if (split[0].equals("-timedwait")) {
            op = new TimedWait(lock);
        } else if (split[0].equals("-timedpark")) {
            op = new TimedPark();
        } else if (split[0].equals("-syncandwork")) {
            op = new SyncAndWork(lock);
        } else if (split[0].equals("-queuedwait")) {
            op = new QueuedWait(semaphore);
        } else {
            throw new IllegalArgumentException("Unknown arg " + arg);
        }

        if (in == null) {
            in = new HashMap<Operation, Double>();
        }
        in.put(op, d);

        return in;
    }

    private static void normalize(Map<Operation, Double> map) {
        double sum = 0;
        for (Double d : map.values()) {
            sum += d;
        }
        if (sum == 0) {
            throw new RuntimeException("No elements!");
        }
        if (sum != 1.0) {
            // Avoid ConcurrentModificationException.
            Set<Operation> tmp = new HashSet<>(map.keySet());
            for (Operation op : tmp) {
                map.put(op, map.get(op) / sum);
            }
        }
    }

    public static void parseAndRun(String[] args) throws Exception {
        int numberOfThreads = -1;
        int numberOfDaemons = -1;
        int totalOperations = -1;
        int operationsPerThread = -1;
        int permits = -1;
        Object lock = new Object();
        Map<Operation, Double> frequencyMap = null;
        boolean dumpMap = false;

        if (args != null) {
            // args[0] is libarttest
            for (int i = 1; i < args.length; i++) {
                if (args[i].equals("-n")) {
                    i++;
                    numberOfThreads = Integer.parseInt(args[i]);
                } else if (args[i].equals("-d")) {
                    i++;
                    numberOfDaemons = Integer.parseInt(args[i]);
                } else if (args[i].equals("-o")) {
                    i++;
                    totalOperations = Integer.parseInt(args[i]);
                } else if (args[i].equals("-t")) {
                    i++;
                    operationsPerThread = Integer.parseInt(args[i]);
                } else if (args[i].equals("-p")) {
                    i++;
                    permits = Integer.parseInt(args[i]);
                } else if (args[i].equals("--locks-only")) {
                    frequencyMap = createLockFrequencyMap(lock);
                } else if (args[i].equals("--allocs-only")) {
                    frequencyMap = createAllocFrequencyMap();
                } else if (args[i].equals("--dumpmap")) {
                    dumpMap = true;
                } else {
                    // Processing an argument of the form "-<operation>:X"
                    // (where X is a double value).
                    Semaphore semaphore = getSemaphore(permits);
                    frequencyMap = updateFrequencyMap(frequencyMap, lock, semaphore, args[i]);
                }
            }
        }

        if (totalOperations != -1 && operationsPerThread != -1) {
            throw new IllegalArgumentException(
                    "Specified both totalOperations and operationsPerThread");
        }

        if (numberOfThreads == -1) {
            numberOfThreads = 5;
        }

        if (numberOfDaemons == -1) {
            numberOfDaemons = 3;
        }

        if (totalOperations == -1) {
            totalOperations = 1000;
        }

        if (operationsPerThread == -1) {
            operationsPerThread = totalOperations/numberOfThreads;
        }

        if (frequencyMap == null) {
            Semaphore semaphore = getSemaphore(permits);
            frequencyMap = createDefaultFrequencyMap(lock, semaphore);
        }
        normalize(frequencyMap);

        if (dumpMap) {
            System.out.println(frequencyMap);
        }

        try {
            runTest(numberOfThreads, numberOfDaemons, operationsPerThread, lock, frequencyMap);
        } catch (Throwable t) {
            // In this case, the output should not contain all the required
            // "Finishing worker" lines.
            Main.printThrowable(t);
        }
    }

    private static Semaphore getSemaphore(int permits) {
        if (permits == -1) {
            // Default number of permits.
            permits = 3;
        }

        Semaphore semaphore = new Semaphore(permits, /* fair */ true);
        forceTransitiveClassInitialization(semaphore, permits);
        return semaphore;
    }

    // Force ahead-of-time initialization of classes used by Semaphore
    // code. Try to exercise all code paths likely to be taken during
    // the actual test later (including having a thread blocking on
    // the semaphore trying to acquire a permit), so that we increase
    // the chances to initialize all classes indirectly used by
    // QueuedWait (e.g. AbstractQueuedSynchronizer$Node).
    private static void forceTransitiveClassInitialization(Semaphore semaphore, final int permits) {
        // Ensure `semaphore` has the expected number of permits
        // before we start.
        assert semaphore.availablePermits() == permits;

        // Let the main (current) thread acquire all permits from
        // `semaphore`. Then create an auxiliary thread acquiring a
        // permit from `semaphore`, blocking because none is
        // available. Have the main thread release one permit, thus
        // unblocking the second thread.

        // Auxiliary thread.
        Thread auxThread = new Thread("Aux") {
            public void run() {
                try {
                    // Try to acquire one permit, and block until
                    // that permit is released by the main thread.
                    semaphore.acquire();
                    // When unblocked, release the acquired permit
                    // immediately.
                    semaphore.release();
                } catch (InterruptedException ignored) {
                    throw new RuntimeException("Test set up failed in auxiliary thread");
                }
            }
        };

        // Main thread.
        try {
            // Acquire all permits.
            semaphore.acquire(permits);
            // Start the auxiliary thread and have it try to acquire a
            // permit.
            auxThread.start();
            // Synchronization: Wait until the auxiliary thread is
            // blocked trying to acquire a permit from `semaphore`.
            while (!semaphore.hasQueuedThreads()) {
                Thread.sleep(100);
            }
            // Release one permit, thus unblocking `auxThread` and let
            // it acquire a permit.
            semaphore.release();
            // Synchronization: Wait for the auxiliary thread to die.
            auxThread.join();
            // Release remaining permits.
            semaphore.release(permits - 1);

            // Verify that all permits have been released.
            assert semaphore.availablePermits() == permits;
        } catch (InterruptedException ignored) {
            throw new RuntimeException("Test set up failed in main thread");
        }
    }

    public static void runTest(final int numberOfThreads, final int numberOfDaemons,
                               final int operationsPerThread, final Object lock,
                               Map<Operation, Double> frequencyMap) throws Exception {
        final Thread mainThread = Thread.currentThread();
        final Barrier startBarrier = new Barrier(numberOfThreads + numberOfDaemons + 1);

        // Each normal thread is going to do operationsPerThread
        // operations. Each daemon thread will loop over all
        // the operations and will not stop.
        // The distribution of operations is determined by
        // the frequencyMap values. We fill out an Operation[]
        // for each thread with the operations it is to perform. The
        // Operation[] is shuffled so that there is more random
        // interactions between the threads.

        // Fill in the Operation[] array for each thread by laying
        // down references to operation according to their desired
        // frequency.
        // The first numberOfThreads elements are normal threads, the last
        // numberOfDaemons elements are daemon threads.
        final Main[] threadStresses = new Main[numberOfThreads + numberOfDaemons];
        for (int t = 0; t < threadStresses.length; t++) {
            Operation[] operations = new Operation[operationsPerThread];
            int o = 0;
            LOOP:
            while (true) {
                for (Operation op : frequencyMap.keySet()) {
                    int freq = (int)(frequencyMap.get(op) * operationsPerThread);
                    for (int f = 0; f < freq; f++) {
                        if (o == operations.length) {
                            break LOOP;
                        }
                        operations[o] = op;
                        o++;
                    }
                }
            }
            // Randomize the operation order
            Collections.shuffle(Arrays.asList(operations));
            threadStresses[t] = (t < numberOfThreads)
                    ? new Main(lock, t, operations)
                    : new Daemon(lock, t, operations, mainThread, startBarrier);
        }

        // Enable to dump operation counts per thread to make sure its
        // sane compared to frequencyMap.
        if (DEBUG) {
            for (int t = 0; t < threadStresses.length; t++) {
                Operation[] operations = threadStresses[t].operations;
                Map<Operation, Integer> distribution = new HashMap<Operation, Integer>();
                for (Operation operation : operations) {
                    Integer ops = distribution.get(operation);
                    if (ops == null) {
                        ops = 1;
                    } else {
                        ops++;
                    }
                    distribution.put(operation, ops);
                }
                System.out.println("Distribution for " + t);
                for (Operation op : frequencyMap.keySet()) {
                    System.out.println(op + " = " + distribution.get(op));
                }
            }
        }

        // Create the runners for each thread. The runner Thread
        // ensures that thread that exit due to operation Exit will be
        // restarted until they reach their desired
        // operationsPerThread.
        Thread[] runners = new Thread[numberOfThreads];
        for (int r = 0; r < runners.length; r++) {
            final Main ts = threadStresses[r];
            runners[r] = new Thread("Runner thread " + r) {
                final Main threadStress = ts;
                public void run() {
                    try {
                        int id = threadStress.id;
                        // No memory hungry task are running yet, so println() should succeed.
                        System.out.println("Starting worker for " + id);
                        // Wait until all runners and daemons reach the starting point.
                        startBarrier.await();
                        // Run the stress tasks.
                        while (threadStress.nextOperation < operationsPerThread) {
                            try {
                                Thread thread = new Thread(ts, "Worker thread " + id);
                                thread.start();
                                thread.join();

                                if (DEBUG) {
                                    System.out.println(
                                        "Thread exited for " + id + " with " +
                                        (operationsPerThread - threadStress.nextOperation) +
                                        " operations remaining.");
                                }
                            } catch (OutOfMemoryError e) {
                                // Ignore OOME since we need to print "Finishing worker"
                                // for the test to pass. This OOM can come from creating
                                // the Thread or from the DEBUG output.
                                // Note that the Thread creation may fail repeatedly,
                                // preventing the runner from making any progress,
                                // especially if the number of daemons is too high.
                            }
                        }
                        // Print "Finishing worker" through JNI to avoid OOME.
                        Main.printString(Main.finishingWorkerMessage);
                    } catch (Throwable t) {
                        Main.printThrowable(t);
                        // Interrupt the main thread, so that it can orderly shut down
                        // instead of waiting indefinitely for some Barrier.
                        mainThread.interrupt();
                    }
                }
            };
        }

        // The notifier thread is a daemon just loops forever to wake
        // up threads in operations Wait and Park.
        if (lock != null) {
            Thread notifier = new Thread("Notifier") {
                public void run() {
                    while (true) {
                        synchronized (lock) {
                            lock.notifyAll();
                        }
                        for (Thread runner : runners) {
                          if (runner != null) {
                            LockSupport.unpark(runner);
                          }
                        }
                    }
                }
            };
            notifier.setDaemon(true);
            notifier.start();
        }

        // Create and start the daemon threads.
        for (int r = 0; r < numberOfDaemons; r++) {
            Main daemon = threadStresses[numberOfThreads + r];
            Thread t = new Thread(daemon, "Daemon thread " + daemon.id);
            t.setDaemon(true);
            t.start();
        }

        for (int r = 0; r < runners.length; r++) {
            runners[r].start();
        }
        // Wait for all threads to reach the starting point.
        startBarrier.await();
        // Wait for runners to finish.
        for (int r = 0; r < runners.length; r++) {
            runners[r].join();
        }
    }

    protected final Operation[] operations;
    private final Object lock;
    protected final int id;

    private int nextOperation;

    private Main(Object lock, int id, Operation[] operations) {
        this.lock = lock;
        this.id = id;
        this.operations = operations;
    }

    public void run() {
        try {
            if (DEBUG) {
                System.out.println("Starting ThreadStress " + id);
            }
            while (nextOperation < operations.length) {
                Operation operation = operations[nextOperation];
                if (DEBUG) {
                    System.out.println("ThreadStress " + id
                                       + " operation " + nextOperation
                                       + " is " + operation);
                }
                nextOperation++;
                if (!operation.perform()) {
                    return;
                }
            }
        } finally {
            if (DEBUG) {
                System.out.println("Finishing ThreadStress for " + id);
            }
        }
    }

    private static class Daemon extends Main {
        private Daemon(Object lock,
                       int id,
                       Operation[] operations,
                       Thread mainThread,
                       Barrier startBarrier) {
            super(lock, id, operations);
            this.mainThread = mainThread;
            this.startBarrier = startBarrier;
        }

        public void run() {
            try {
                if (DEBUG) {
                    System.out.println("Starting ThreadStress Daemon " + id);
                }
                startBarrier.await();
                try {
                    int i = 0;
                    while (true) {
                        Operation operation = operations[i];
                        if (DEBUG) {
                            System.out.println("ThreadStress Daemon " + id
                                               + " operation " + i
                                               + " is " + operation);
                        }
                        // Ignore the result of the performed operation, making
                        // Exit.perform() essentially a no-op for daemon threads.
                        operation.perform();
                        i = (i + 1) % operations.length;
                    }
                } catch (OutOfMemoryError e) {
                    // Catch OutOfMemoryErrors since these can cause the test to fail it they print
                    // the stack trace after "Finishing worker". Note that operations should catch
                    // their own OOME, this guards only agains OOME in the DEBUG output.
                }
                if (DEBUG) {
                    System.out.println("Finishing ThreadStress Daemon for " + id);
                }
            } catch (Throwable t) {
                Main.printThrowable(t);
                // Interrupt the main thread, so that it can orderly shut down
                // instead of waiting indefinitely for some Barrier.
                mainThread.interrupt();
            }
        }

        final Thread mainThread;
        final Barrier startBarrier;
    }

    // Note: java.util.concurrent.CyclicBarrier.await() allocates memory and may throw OOM.
    // That is highly undesirable in this test, so we use our own simple barrier class.
    // The only memory allocation that can happen here is the lock inflation which uses
    // a native allocation. As such, it should succeed even if the Java heap is full.
    // If the native allocation surprisingly fails, the program shall abort().
    private static class Barrier {
        public Barrier(int initialCount) {
            count = initialCount;
        }

        public synchronized void await() throws InterruptedException {
            --count;
            if (count != 0) {
                do {
                    wait();
                } while (count != 0);  // Check for spurious wakeup.
            } else {
                notifyAll();
            }
        }

        private int count;
    }

    // Printing a String/Throwable through JNI requires only native memory and space
    // in the local reference table, so it should succeed even if the Java heap is full.
    private static native void printString(String s);
    private static native void printThrowable(Throwable t);

    static final String finishingWorkerMessage;
    static final String errnoExceptionName;
    static {
        // We pre-allocate the strings in class initializer to avoid const-string
        // instructions in code using these strings later as they may throw OOME.
        finishingWorkerMessage = "Finishing worker\n";
        errnoExceptionName = "ErrnoException";
    }
}
