/*******************************************************************************
 * Copyright (c) 1998, 2015 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Oracle - initial API and implementation from Oracle TopLink
 ******************************************************************************/
package org.eclipse.persistence.testing.framework;

import java.util.*;

/**
 * This test compares the concurrency of an operation defined run().
 * It compares the performance of performing the task with 1, 2, 4, and 16 threads.
 * On a single CPU machine all of the results should be similar,
 * on a multi-CPU machine the 2,4,16 should be faster if the task can be performed concurrently.
 */
public abstract class ConcurrentPerformanceComparisonTest extends PerformanceComparisonTestCase {
    public static double NUMBER_OF_CPUS = 1.1; // 0.1 for hyper-threading.
    public static int DEFAULT_THREADS = 32;
    protected int minThreads;
    protected int maxThreads;
    protected Exception caughtException;
    protected List workerThreads;

    public ConcurrentPerformanceComparisonTest() {
        this.minThreads = 2;
        this.maxThreads = DEFAULT_THREADS;
    }

    /**
     * Return the maximum number of threads the test should run to.
     */
    public int getMaxThreads() {
        return maxThreads;
    }

    /**
     * Set the maximum number of threads the test should run to.
     */
    public void setMaxThreads(int maxThreads) {
        this.maxThreads = maxThreads;
    }

    /**
     * Reset the iteration count.
     * This is maintained by the test to allow concurrent tests to manage the count.
     */
    public synchronized void resetIterations() {
        this.iterations = 0;
    }

    /**
     * Start 1 thread test.
     */
    public void startTest() {
        startTest(1);
    }

    /**
     * Start each worker thread.
     */
    public void startTest(int numberOfThreads) {
        caughtException = null;
        // Spawn worker threads.
        for (int index = 0; index < (numberOfThreads - 1); index++) {
            WorkerThread thread = (WorkerThread)getWorkerThreads().get(index);
            thread.resumeExecution();
        }
    }

    /**
     * Allows any test specific setup before starting the test run.
     */
    public void endTest() {
        // Suspend the worker threads,
        // suspend all no matter what number of threads was to even out performance.
        for (int index = 0; index < getMaxThreads(); index++) {
            WorkerThread thread = (WorkerThread)getWorkerThreads().get(index);
            thread.suspendExecution();
        }
        // Let workers finish.
        Thread.yield();
        // Wait for workers to finish,
        // wait all no matter what number of threads was to even out performance.
        for (int index = 0; index < getMaxThreads(); index++) {
            WorkerThread thread = (WorkerThread)getWorkerThreads().get(index);
            thread.joinExecution();
        }
    }

    public List getWorkerThreads() {
        return workerThreads;
    }

    /**
     * Start the worker threads.
     */
    public void setup() {
        int threads = this.minThreads;
        while (threads <= getMaxThreads()) {
            this.addThreadTest(threads);
            threads = threads * 2;
        }
        this.workerThreads = new ArrayList(getMaxThreads());
        for (int index = 0; index < getMaxThreads(); index++) {
            WorkerThread thread = new WorkerThread();
            this.workerThreads.add(thread);
            thread.start();
        }
    }

    /**
     * Count REPEATS runs of the run method with 1 thread.
     */
    public void test() throws Exception {
        test(1);
    }

    /**
     * Count REPEATS runs of the run method with n threads.
     */
    public void test(int numberOfThreads) throws Exception {
        if (caughtException != null) {
            throw caughtException;
        }
        runTask();
    }

    /**
     * Stop the worker threads.
     */
    public void reset() {
        // Stop the worker threads.
        for (int index = 0; index < getWorkerThreads().size(); index++) {
            WorkerThread thread = (WorkerThread)getWorkerThreads().get(index);
            thread.stopExecution();
        }
        // Let workers finish.
        Thread.yield();
        this.workerThreads = null;
    }

    /**
     * Verify the multi-threaded results are NUMBER_OF_CPU times better
     * than the single thread, allowing for the allowableDecrease.
     */
    public void verify() {
        PerformanceComparisonTestResult result = (PerformanceComparisonTestResult)getTestResult();
        for (int index = 0; index < result.percentageDifferences.size(); index++) {
            PerformanceComparisonTest test = (PerformanceComparisonTest)getTests().get(index);
            double allowable =
                (Math.max(Math.min(NUMBER_OF_CPUS, index + 1), NUMBER_OF_CPUS) * 100) - 100 + (getAllowableDecrease() *
                                                                                               (index + 1));
            test.setAllowableDecrease(allowable);
        }
        super.verify();
    }

    /**
     * Perform the task.
     */
    public abstract void runTask() throws Exception;

    /**
     * Spawn numberOfThreads to run the test's run method and count total number
     * of operations.
     */
    public void addThreadTest(final int numberOfThreads) {
        PerformanceComparisonTestCase test = new PerformanceComparisonTestCase() {
                public void startTest() {
                    ConcurrentPerformanceComparisonTest.this.startTest(numberOfThreads);
                }

                public void test() throws Exception {
                    ConcurrentPerformanceComparisonTest.this.test(numberOfThreads);
                }

                public void endTest() {
                    ConcurrentPerformanceComparisonTest.this.endTest();
                }
            };
        test.setName("ThreadTest:" + numberOfThreads);
        addTest(test);
    }

    /**
     * Defines the work thread.
     * A worker thread calls the run method in a loop,
     * until it is suspended.
     */
    protected class WorkerThread extends Thread {
        protected volatile boolean isSuspended = true;
        protected volatile boolean isDead = false;

        /**
         * After the next completion of the run method, suspend execution.
         * The thread is still alive, but waiting to be signaled to resumed.
         */
        public void stopExecution() {
            isDead = true;
        }

        /**
         * After the next completion of the run method, suspend execution.
         * The thread is still alive, but waiting to be signaled to resumed.
         */
        public void suspendExecution() {
            isSuspended = true;
        }

        /**
         * Wait for the thread to suspend.
         * Synchronize will wait for wait to release synchornization.
         */
        public synchronized void joinExecution() {
            if (!isSuspended) {
                throw new RuntimeException("Must suspend first");
            }
        }

        /**
         * Continue running the run method.
         */
        public synchronized void resumeExecution() {
            isSuspended = false;
            try {
                notify();
            } catch (Exception exception) {
                throw new RuntimeException(exception.getMessage());
            }
        }

        /**
         * Run the test run method in a loop until killed.
         */
        public synchronized void run() {
            try {
                while (!isDead) {
                    // Allows the thread to suspend itself when the current test is done.
                    if (isSuspended) {
                        wait();
                    }
                    ConcurrentPerformanceComparisonTest.this.runTask();
                    ConcurrentPerformanceComparisonTest.this.incrementIterations();
                }
            } catch (Exception exception) {
                ConcurrentPerformanceComparisonTest.this.caughtException = exception;
            }
        }
    }
}
