import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.text.SimpleDateFormat;

/**
 * Attempt various strategies for making SimpleDateFormat safe, and report the
 * runtime of each approach.
 *
 * @author <a href="jesse@swank.ca">Jesse Wilson</a>
 */
public class SDFPerformance {

    /** the pattern used when encoding dates */
    private static final String SIMPLE_DATE_FORMAT_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z";

    /**
     * Describe a strategy to make SimpleDateFormat safe.
     */
    public interface DateFormatAccess {
        public String format(Date date);
    }

    /**
     * Create a new SimpleDateFormat on every invocation.
     */
    public static class SharedDateFormatAccess implements DateFormatAccess {
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN);
        public String format(Date date) {
            return simpleDateFormat.format(date);
        }
    }

    /**
     * Create a new SimpleDateFormat on every invocation.
     */
    public static class NewInstanceDateFormatAccess implements DateFormatAccess {
        public String format(Date date) {
            return new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(date);
        }
    }

    /**
     * Use the synchronized() keyword.
     */
    public static class SynchronizedInstanceDateFormatAccess implements DateFormatAccess {
        private SimpleDateFormat dateFormat = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN);
        public synchronized String format(Date date) {
            return dateFormat.format(date);
        }
    }

    /**
     * Use a pool of SimpleDateFormat objects, implemented using an ArrayList.
     */
    public static class ListPoolDateFormatAccess implements DateFormatAccess {
        private List pool = new ArrayList(10);

        public String format(Date date) {
            SimpleDateFormat dateFormat;
            synchronized(pool) {
                int size = pool.size();
                if(size == 0) dateFormat = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN);
                else dateFormat = (SimpleDateFormat)pool.remove(size - 1);
            }
            String result = dateFormat.format(date);
            synchronized(pool) {
                pool.add(dateFormat);
            }
            return result;
        }
    }

    /**
     * Use ThreadLocals for the implementation.
     */
    public static class ThreadLocalDateFormatAccess implements DateFormatAccess {
        ThreadLocal<SimpleDateFormat> threadLocal = new DateFormatThreadLocal();
        public String format(Date date) {
            return threadLocal.get().format(date);
        }
        private static class DateFormatThreadLocal extends ThreadLocal<SimpleDateFormat> {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN);
            }
        }
    }

    /**
     * Run a sequence of warm ups followed by a sequence of tests.
     */
    public static void main(String[] args) {
        System.out.println("WARM UP: 100 threads x 1000 executions");
        System.out.println(test(new SharedDateFormatAccess(), 1000, 100));
        System.out.println(test(new NewInstanceDateFormatAccess(), 1000, 100));
        System.out.println(test(new SynchronizedInstanceDateFormatAccess(), 1000, 100));
        System.out.println(test(new ListPoolDateFormatAccess(), 1000, 100));
        System.out.println(test(new ThreadLocalDateFormatAccess(), 1000, 100));

        System.out.println("RUN: 100 threads x 1000 executions");
        System.out.println(test(new SharedDateFormatAccess(), 1000, 100));
        System.out.println(test(new NewInstanceDateFormatAccess(), 1000, 100));
        System.out.println(test(new SynchronizedInstanceDateFormatAccess(), 1000, 100));
        System.out.println(test(new ListPoolDateFormatAccess(), 1000, 100));
        System.out.println(test(new ThreadLocalDateFormatAccess(), 1000, 100));
    }

    /**
     * Test a particular implemenation and return the result.
     */
    private static TestResult test(final DateFormatAccess dateFormatAccess, final int roundsPerThread, final int threadCount) {
        final TestResult testResult = new TestResult();
        testResult.input = dateFormatAccess.getClass();
        testResult.roundsPerThread = roundsPerThread;
        testResult.threadCount = threadCount;

        final Date dateA = new Date(System.currentTimeMillis());
        final Date dateB = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 7));
        final Date dateC = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 31));
        final String expectedA = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(dateA);
        final String expectedB = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(dateB);
        final String expectedC = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(dateC);

        final Thread[] threads = new Thread[threadCount];
        for(int i = 0; i < threadCount; i++) {
            final int previous = i - 1;
            threads[i] = new Thread(new Runnable() {
                public void run() {
                    try {
                        for(int j = 0; j < roundsPerThread; j++) {
                            String resultA = dateFormatAccess.format(dateA);
                            if(!expectedA.equals(resultA)) throw new IllegalStateException("Expected " + expectedA + " but found " + resultA);
                            String resultB = dateFormatAccess.format(dateB);
                            if(!expectedA.equals(resultA)) throw new IllegalStateException("Expected " + expectedB + " but found " + resultB);
                            String resultC = dateFormatAccess.format(dateC);
                            if(!expectedA.equals(resultA)) throw new IllegalStateException("Expected " + expectedC + " but found " + resultC);
                            Thread.sleep(1);
                        }
                        if(previous > 0) threads[previous].join();
                    } catch (RuntimeException e) {
                        testResult.exceptions.add(e);
                    } catch (InterruptedException e) {
                        System.exit(1);
                    }
                }
            });
        }

        long start = System.currentTimeMillis();
        for(int i = 0; i < threadCount; i++) {
            threads[i].start();
        }
        try {
            threads[threadCount - 1].join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();

        testResult.runtime = end - start;
        return testResult;
    }

    /**
     * The consequence of a test execution.
     */
    private static class TestResult {
        int threadCount;
        int roundsPerThread;
        Class input;
        long runtime;
        final List<RuntimeException> exceptions = new ArrayList<RuntimeException>();

        public String toString() {
            if(exceptions.isEmpty()) {
                return "threads: " + threadCount + ", rounds: " + roundsPerThread + ", implementation " + input.getName() + ": time: " + runtime + "ms";
            } else {
                return "threads: " + threadCount + ", rounds: " + roundsPerThread + ", implementation " + input.getName() + " failed";
            }
        }
    }
}
