/*
 * 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 java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

// Run on host with:
//   javac ThreadTest.java && java ThreadStress && rm *.class
public class Main implements Runnable {

    public static final boolean DEBUG = false;

    enum Operation {
        OOM(1),
        SIGQUIT(19),
        ALLOC(60),
        STACKTRACE(20),
        EXIT(50),

        SLEEP(25),
        TIMED_WAIT(10),
        WAIT(15);

        private final int frequency;
        Operation(int frequency) {
            this.frequency = frequency;
        }
    }

    public static void main(String[] args) throws Exception {

        final int numberOfThreads = 5;
        final int totalOperations = 1000;
        final int operationsPerThread = totalOperations/numberOfThreads;

        // Lock used to notify threads performing Operation.WAIT
        final Object lock = new Object();

        // Each thread is going to do operationsPerThread
        // operations. The distribution of operations is determined by
        // the Operation.frequency 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.

        // The simple-minded filling in of Operation[] based on
        // Operation.frequency below won't have even have close to a
        // reasonable distribution if the count of Operation
        // frequencies is greater than the total number of
        // operations. So here we do a quick sanity check in case
        // people tweak the constants above.
        int operationCount = 0;
        for (Operation op : Operation.values()) {
            operationCount += op.frequency;
        }
        if (operationCount > operationsPerThread) {
            throw new AssertionError(operationCount + " > " + operationsPerThread);
        }

        // Fill in the Operation[] array for each thread by laying
        // down references to operation according to their desired
        // frequency.
        final Main[] threadStresses = new Main[numberOfThreads];
        for (int t = 0; t < threadStresses.length; t++) {
            Operation[] operations = new Operation[operationsPerThread];
            int o = 0;
            LOOP:
            while (true) {
                for (Operation op : Operation.values()) {
                    for (int f = 0; f < op.frequency; f++) {
                        if (o == operations.length) {
                            break LOOP;
                        }
                        operations[o] = op;
                        o++;
                    }
                }
            }
            // Randomize the oepration order
            Collections.shuffle(Arrays.asList(operations));
            threadStresses[t] = new Main(lock, t, operations);
        }

        // Enable to dump operation counds per thread to make sure its
        // sane compared to Operation.frequency
        if (DEBUG) {
            for (int t = 0; t < threadStresses.length; t++) {
                Operation[] operations = new Operation[operationsPerThread];
                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 : Operation.values()) {
                    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() {
                    int id = threadStress.id;
                    System.out.println("Starting worker for " + id);
                    while (threadStress.nextOperation < operationsPerThread) {
                        Thread thread = new Thread(ts, "Worker thread " + id);
                        thread.start();
                        try {
                            thread.join();
                        } catch (InterruptedException e) {
                        }
                        System.out.println("Thread exited for " + id + " with "
                                           + (operationsPerThread - threadStress.nextOperation)
                                           + " operations remaining.");
                    }
                    System.out.println("Finishing worker");
                }
            };
        }

        // The notifier thread is a daemon just loops forever to wake
        // up threads in Operation.WAIT
        Thread notifier = new Thread("Notifier") {
            public void run() {
                while (true) {
                    synchronized (lock) {
                        lock.notifyAll();
                    }
                }
            }
        };
        notifier.setDaemon(true);
        notifier.start();

        for (int r = 0; r < runners.length; r++) {
            runners[r].start();
        }
        for (int r = 0; r < runners.length; r++) {
            runners[r].join();
        }
    }

    private final Operation[] operations;
    private final Object lock;
    private 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++;
                switch (operation) {
                    case EXIT: {
                        return;
                    }
                    case SIGQUIT: {
                        try {
                            SIGQUIT();
                        } catch (Exception ex) {
                        }
                    }
                    case SLEEP: {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException ignored) {
                        }
                    }
                    case TIMED_WAIT: {
                        synchronized (lock) {
                            try {
                                lock.wait(100, 0);
                            } catch (InterruptedException ignored) {
                            }
                        }
                        break;
                    }
                    case WAIT: {
                        synchronized (lock) {
                            try {
                                lock.wait();
                            } catch (InterruptedException ignored) {
                            }
                        }
                        break;
                    }
                    case OOM: {
                        try {
                            List<byte[]> l = new ArrayList<byte[]>();
                            while (true) {
                                l.add(new byte[1024]);
                            }
                        } catch (OutOfMemoryError e) {
                        }
                        break;
                    }
                    case ALLOC: {
                        try {
                            List<byte[]> l = new ArrayList<byte[]>();
                            for (int i = 0; i < 1024; i++) {
                                l.add(new byte[1024]);
                            }
                        } catch (OutOfMemoryError e) {
                        }
                        break;
                    }
                    case STACKTRACE: {
                        Thread.currentThread().getStackTrace();
                        break;
                    }
                    default: {
                        throw new AssertionError(operation.toString());
                    }
                }
            }
        } finally {
            if (DEBUG) {
                System.out.println("Finishing ThreadStress for " + id);
            }
        }
    }

    private static void SIGQUIT() throws Exception {
        Class<?> osClass = Class.forName("android.system.Os");
        Method getpid = osClass.getDeclaredMethod("getpid");
        int pid = (Integer)getpid.invoke(null);

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

        Method kill = osClass.getDeclaredMethod("kill", int.class, int.class);
        kill.invoke(null, pid, sigquit);
    }
}
