/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */
package org.apache.catalina.valves;

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

import org.junit.Test;

/**
 * Some simple micro-benchmarks to help determine best approach for thread safety in valves, particularly the
 * {@link AccessLogValve}. Implemented as JUnit tests to make the simple to execute but does not used Test* as the class
 * name to avoid being included in the automated unit tests.
 */
public class Benchmarks {
    @Test
    public void testAccessLogGetDate() throws Exception {
        // Is it better to use a sync or a thread local here?
        BenchmarkTest benchmark = new BenchmarkTest();
        Runnable[] tests = new Runnable[] { new GetDateBenchmarkTest_Sync(), new GetDateBenchmarkTest_Local(),
                new GetDateBenchmarkTest_LocalMutableLong(), new GetDateBenchmarkTest_LocalStruct() };
        benchmark.doTest(5, tests);
    }

    private static class GetDateBenchmarkTest_Sync implements Runnable {

        @Override
        public String toString() {
            return "Syncs";
        }

        private volatile long currentMillis = 0;
        private volatile Date currentDate = null;

        @Override
        public void run() {
            getCurrentDate();
        }

        public Date getCurrentDate() {
            long systime = System.currentTimeMillis();
            if ((systime - currentMillis) > 1000) {
                synchronized (this) {
                    if ((systime - currentMillis) > 1000) {
                        currentDate = new Date(systime);
                        currentMillis = systime;
                    }
                }
            }
            return currentDate;
        }
    }

    private static class GetDateBenchmarkTest_Local implements Runnable {

        @Override
        public String toString() {
            return "ThreadLocals";
        }

        private ThreadLocal<Long> currentMillisLocal = new ThreadLocal<Long>() {
            @Override
            protected Long initialValue() {
                return Long.valueOf(0);
            }
        };

        private ThreadLocal<Date> currentDateLocal = new ThreadLocal<>();

        @Override
        public void run() {
            getCurrentDate();
        }

        public Date getCurrentDate() {
            long systime = System.currentTimeMillis();
            if ((systime - currentMillisLocal.get().longValue()) > 1000) {
                currentDateLocal.set(new Date(systime));
                currentMillisLocal.set(Long.valueOf(systime));
            }
            return currentDateLocal.get();
        }
    }

    private static class GetDateBenchmarkTest_LocalMutableLong implements Runnable {

        @Override
        public String toString() {
            return "ThreadLocals with a mutable Long";
        }

        private static class MutableLong {
            long value = 0;
        }

        private ThreadLocal<MutableLong> currentMillisLocal = new ThreadLocal<MutableLong>() {
            @Override
            protected MutableLong initialValue() {
                return new MutableLong();
            }
        };

        private ThreadLocal<Date> currentDateLocal = new ThreadLocal<>();

        @Override
        public void run() {
            getCurrentDate();
        }

        public Date getCurrentDate() {
            long systime = System.currentTimeMillis();
            if ((systime - currentMillisLocal.get().value) > 1000) {
                currentDateLocal.set(new Date(systime));
                currentMillisLocal.get().value = systime;
            }
            return currentDateLocal.get();
        }
    }

    private static class GetDateBenchmarkTest_LocalStruct implements Runnable {

        @Override
        public String toString() {
            return "single ThreadLocal";
        }

        // note, that we can avoid (long -> Long) conversion
        private static class Struct {
            public long currentMillis = 0;
            public Date currentDate;
        }

        private ThreadLocal<Struct> currentStruct = new ThreadLocal<Struct>() {
            @Override
            protected Struct initialValue() {
                return new Struct();
            }
        };

        @Override
        public void run() {
            getCurrentDate();
        }

        public Date getCurrentDate() {
            Struct struct = currentStruct.get();
            long systime = System.currentTimeMillis();
            if ((systime - struct.currentMillis) > 1000) {
                struct.currentDate = new Date(systime);
                struct.currentMillis = systime;
            }
            return struct.currentDate;
        }
    }

    @Test
    public void testAccessLogTimeDateElement() throws Exception {
        // Is it better to use a sync or a thread local here?
        BenchmarkTest benchmark = new BenchmarkTest();
        Runnable[] tests = new Runnable[] { new TimeDateElementBenchmarkTest_Sync(),
                new TimeDateElementBenchmarkTest_Local(), new TimeDateElementBenchmarkTest_LocalStruct(),
                new TimeDateElementBenchmarkTest_LocalStruct_SBuilder() };
        benchmark.doTest(5, tests);
    }

    private abstract static class TimeDateElementBenchmarkTestBase {
        protected static final String months[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
                "Nov", "Dec" };

        protected String lookup(String month) {
            int index;
            try {
                index = Integer.parseInt(month) - 1;
            } catch (Throwable t) {
                index = 0; // cannot happen, in theory
            }
            return months[index];
        }
    }

    private static class TimeDateElementBenchmarkTest_Sync extends TimeDateElementBenchmarkTestBase
            implements Runnable {

        @Override
        public String toString() {
            return "Syncs";
        }

        private volatile Date currentDate = new Date();
        private volatile String currentDateString = null;
        private SimpleDateFormat dayFormatter = new SimpleDateFormat("dd");
        private SimpleDateFormat monthFormatter = new SimpleDateFormat("MM");
        private SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy");
        private SimpleDateFormat timeFormatter = new SimpleDateFormat("hh:mm:ss");

        @Override
        public void run() {
            printDate();
        }

        public String printDate() {
            StringBuilder buf = new StringBuilder();
            Date date = getDateSync();
            if (currentDate != date) {
                synchronized (this) {
                    if (currentDate != date) {
                        StringBuilder current = new StringBuilder(32);
                        current.append('[');
                        current.append(dayFormatter.format(date)); // Day
                        current.append('/');
                        current.append(lookup(monthFormatter.format(date))); // Month
                        current.append('/');
                        current.append(yearFormatter.format(date)); // Year
                        current.append(':');
                        current.append(timeFormatter.format(date)); // Time
                        current.append(']');
                        currentDateString = current.toString();
                        currentDate = date;
                    }
                }
            }
            buf.append(currentDateString);
            return buf.toString();
        }

        private Date getDateSync() {
            long systime = System.currentTimeMillis();
            if ((systime - currentDate.getTime()) > 1000) {
                synchronized (this) {
                    if ((systime - currentDate.getTime()) > 1000) {
                        currentDate.setTime(systime);
                    }
                }
            }
            return currentDate;
        }
    }

    private static class TimeDateElementBenchmarkTest_Local extends TimeDateElementBenchmarkTestBase
            implements Runnable {

        @Override
        public String toString() {
            return "ThreadLocals";
        }

        private ThreadLocal<String> currentDateStringLocal = new ThreadLocal<>();

        private ThreadLocal<Date> currentDateLocal = new ThreadLocal<Date>() {
            @Override
            protected Date initialValue() {
                return new Date();
            }
        };
        private ThreadLocal<SimpleDateFormat> dayFormatterLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("dd");
            }
        };
        private ThreadLocal<SimpleDateFormat> monthFormatterLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("MM");
            }
        };
        private ThreadLocal<SimpleDateFormat> yearFormatterLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy");
            }
        };
        private ThreadLocal<SimpleDateFormat> timeFormatterLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("hh:mm:ss");
            }
        };

        @Override
        public void run() {
            printDate();
        }

        public String printDate() {
            getDateLocal();
            if (currentDateStringLocal.get() == null) {
                StringBuilder current = new StringBuilder(32);
                current.append('[');
                current.append(dayFormatterLocal.get().format(currentDateLocal.get())); // Day
                current.append('/');
                current.append(lookup(monthFormatterLocal.get().format(currentDateLocal.get()))); // Month
                current.append('/');
                current.append(yearFormatterLocal.get().format(currentDateLocal.get())); // Year
                current.append(':');
                current.append(timeFormatterLocal.get().format(currentDateLocal.get())); // Time
                current.append(']');
                currentDateStringLocal.set(current.toString());
            }
            return currentDateStringLocal.get();
        }

        private Date getDateLocal() {
            long systime = System.currentTimeMillis();
            if ((systime - currentDateLocal.get().getTime()) > 1000) {
                currentDateLocal.get().setTime(systime);
                currentDateStringLocal.set(null);
            }
            return currentDateLocal.get();
        }
    }

    private static class TimeDateElementBenchmarkTest_LocalStruct extends TimeDateElementBenchmarkTestBase
            implements Runnable {

        @Override
        public String toString() {
            return "single ThreadLocal";
        }

        private static class Struct {
            public String currentDateString;
            public Date currentDate = new Date();
            public SimpleDateFormat dayFormatter = new SimpleDateFormat("dd");
            public SimpleDateFormat monthFormatter = new SimpleDateFormat("MM");
            public SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy");
            public SimpleDateFormat timeFormatter = new SimpleDateFormat("hh:mm:ss");
        }

        private ThreadLocal<Struct> structLocal = new ThreadLocal<Struct>() {
            @Override
            protected Struct initialValue() {
                return new Struct();
            }
        };

        @Override
        public void run() {
            printDate();
        }

        public String printDate() {
            getDateLocal();
            Struct struct = structLocal.get();
            if (struct.currentDateString == null) {
                StringBuilder current = new StringBuilder(32);
                current.append('[');
                current.append(struct.dayFormatter.format(struct.currentDate)); // Day
                current.append('/');
                current.append(lookup(struct.monthFormatter.format(struct.currentDate))); // Month
                current.append('/');
                current.append(struct.yearFormatter.format(struct.currentDate)); // Year
                current.append(':');
                current.append(struct.timeFormatter.format(struct.currentDate)); // Time
                current.append(']');
                struct.currentDateString = current.toString();
            }
            return struct.currentDateString;
        }

        private Date getDateLocal() {
            Struct struct = structLocal.get();
            long systime = System.currentTimeMillis();
            if ((systime - struct.currentDate.getTime()) > 1000) {
                struct.currentDate.setTime(systime);
                struct.currentDateString = null;
            }
            return struct.currentDate;
        }
    }

    private static class TimeDateElementBenchmarkTest_LocalStruct_SBuilder extends TimeDateElementBenchmarkTestBase
            implements Runnable {

        @Override
        public String toString() {
            return "single ThreadLocal, with StringBuilder";
        }

        private static class Struct {
            public String currentDateString;
            public Date currentDate = new Date();
            public SimpleDateFormat dayFormatter = new SimpleDateFormat("dd");
            public SimpleDateFormat monthFormatter = new SimpleDateFormat("MM");
            public SimpleDateFormat yearFormatter = new SimpleDateFormat("yyyy");
            public SimpleDateFormat timeFormatter = new SimpleDateFormat("hh:mm:ss");
        }

        private ThreadLocal<Struct> structLocal = new ThreadLocal<Struct>() {
            @Override
            protected Struct initialValue() {
                return new Struct();
            }
        };

        @Override
        public void run() {
            printDate();
        }

        public String printDate() {
            getDateLocal();
            Struct struct = structLocal.get();
            if (struct.currentDateString == null) {
                StringBuilder current = new StringBuilder(32);
                current.append('[');
                current.append(struct.dayFormatter.format(struct.currentDate)); // Day
                current.append('/');
                current.append(lookup(struct.monthFormatter.format(struct.currentDate))); // Month
                current.append('/');
                current.append(struct.yearFormatter.format(struct.currentDate)); // Year
                current.append(':');
                current.append(struct.timeFormatter.format(struct.currentDate)); // Time
                current.append(']');
                struct.currentDateString = current.toString();
            }
            return struct.currentDateString;
        }

        private Date getDateLocal() {
            Struct struct = structLocal.get();
            long systime = System.currentTimeMillis();
            if ((systime - struct.currentDate.getTime()) > 1000) {
                struct.currentDate.setTime(systime);
                struct.currentDateString = null;
            }
            return struct.currentDate;
        }
    }

    private static class BenchmarkTest {
        public void doTest(int threadCount, Runnable[] tests) throws Exception {
            for (int iterations = 1000000; iterations < 10000001; iterations += 1000000) {
                for (Runnable test : tests) {
                    doTestInternal(threadCount, iterations, test);
                }
            }
        }

        private void doTestInternal(int threadCount, int iterations, Runnable test) throws Exception {
            long start = System.currentTimeMillis();
            Thread[] threads = new Thread[threadCount];
            for (int i = 0; i < threadCount; i++) {
                threads[i] = new Thread(new TestThread(iterations, test));
            }
            for (int i = 0; i < threadCount; i++) {
                threads[i].start();
            }
            for (int i = 0; i < threadCount; i++) {
                threads[i].join();
            }
            long end = System.currentTimeMillis();

            System.out.println(test.getClass().getSimpleName() + ": " + threadCount + " threads and " + iterations +
                    " iterations using " + test + " took " + (end - start) + "ms");
        }
    }

    private static class TestThread implements Runnable {
        private int count;
        private Runnable test;

        TestThread(int count, Runnable test) {
            this.count = count;
            this.test = test;
        }

        @Override
        public void run() {
            for (int i = 0; i < count; i++) {
                test.run();
            }
        }
    }
}
