/*
 * 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.logging.log4j.perf.jmh;

import java.nio.charset.Charset;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;

import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.ThreadContext.ContextStack;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.DefaultConfiguration;
import org.apache.logging.log4j.core.impl.Log4jLogEvent;
import org.apache.logging.log4j.core.pattern.LogEventPatternConverter;
import org.apache.logging.log4j.core.pattern.PatternFormatter;
import org.apache.logging.log4j.core.pattern.PatternParser;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.SimpleMessage;
import org.apache.logging.log4j.util.StringMap;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;

/**
 * Checks PatternLayout performance when reusing the StringBuilder in a ThreadLocal, an ObjectPool or when creating a
 * new instance for each log event.
 */
// ============================== HOW TO RUN THIS TEST: ====================================
// (Quick build: mvn -DskipTests=true clean package -pl log4j-perf -am )
//
// single thread:
// java -jar log4j-perf/target/benchmarks.jar ".*ThreadLocalVsPool.*" -f 1 -wi 10 -i 20 -tu ns -bm sample
//
// four threads:
// java -jar log4j-perf/target/benchmarks.jar ".*ThreadLocalVsPool.*" -f 1 -wi 10 -i 20 -tu ns -bm sample -t 4
//
// Usage help:
// java -jar log4j-perf/target/benchmarks.jar -help
//
@State(Scope.Benchmark)
public class ThreadLocalVsPoolBenchmark {

    static final Charset CHARSET_DEFAULT = Charset.defaultCharset();
    static final String LOG4JPATTERN = "%d %5p [%t] %c{1} %X{transactionId} - %m%n";
    static final int DEFAULT_STRING_BUILDER_SIZE = 1024;

    /**
     * The LogEvent to serialize.
     */
    private final static LogEvent LOG4J2EVENT = createLog4j2Event();

    /**
     * Initial converter for pattern.
     */
    private final static PatternFormatter[] formatters = createFormatters();

    private final StringBuilderPool pool = new StringBuilderPool(DEFAULT_STRING_BUILDER_SIZE);
    private static ThreadLocal<StringBuilder> threadLocal = new ThreadLocal<>();

    /**
     */
    private static PatternFormatter[] createFormatters() {
        final Configuration config = new DefaultConfiguration();
        final PatternParser parser = new PatternParser(config, "Converter", LogEventPatternConverter.class);
        final List<PatternFormatter> result = parser.parse(LOG4JPATTERN, false, true);
        return result.toArray(new PatternFormatter[result.size()]);
    }

    @Benchmark
    public byte[] newInstance() {
        return serializeWithNewInstance(LOG4J2EVENT).getBytes(CHARSET_DEFAULT);
    }

    @Benchmark
    public byte[] threadLocal() {
        return serializeWithThreadLocal(LOG4J2EVENT).getBytes(CHARSET_DEFAULT);
    }

    @Benchmark
    public byte[] objectPool() {
        return serializeWithPool(LOG4J2EVENT).getBytes(CHARSET_DEFAULT);
    }

    @Benchmark
    public String _stringNewInstance() {
        return serializeWithNewInstance(LOG4J2EVENT);
    }

    @Benchmark
    public String _stringThreadLocal() {
        return serializeWithThreadLocal(LOG4J2EVENT);
    }

    @Benchmark
    public String _stringObjectPool() {
        return serializeWithPool(LOG4J2EVENT);
    }

    private String serializeWithNewInstance(final LogEvent event) {
        final StringBuilder buf = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
        return serialize(event, buf);
    }

    private String serializeWithThreadLocal(final LogEvent event) {
        StringBuilder buf = threadLocal.get();
        if (buf == null) {
            buf = new StringBuilder(DEFAULT_STRING_BUILDER_SIZE);
            threadLocal.set(buf);
        }
        buf.setLength(0);
        return serialize(event, buf);
    }

    private String serializeWithPool(final LogEvent event) {
        final StringBuilder buf = pool.borrowObject();
        try {
            buf.setLength(0);
            return serialize(event, buf);
        } finally {
            pool.returnObject(buf);
        }
    }

    private String serialize(final LogEvent event, final StringBuilder buf) {
        final int len = formatters.length;
        for (int i = 0; i < len; i++) {
            formatters[i].format(event, buf);
        }
        return buf.toString();
    }

    private static LogEvent createLog4j2Event() {
        final Marker marker = null;
        final String fqcn = "com.mycom.myproject.mypackage.MyClass";
        final Level level = Level.DEBUG;
        final String STR = "AB!(%087936DZYXQWEIOP$#^~-=/><nb"; // length=32
        final Message message = new SimpleMessage(STR);
        final Throwable t = null;
        final StringMap mdc = null;
        final ContextStack ndc = null;
        final String threadName = null;
        final StackTraceElement location = null;
        final long timestamp = 12345678;

        return Log4jLogEvent.newBuilder() //
                .setLoggerName("name(ignored)") //
                .setMarker(marker) //
                .setLoggerFqcn(fqcn) //
                .setLevel(level) //
                .setMessage(message) //
                .setThrown(t) //
                .setContextData(mdc) //
                .setContextStack(ndc) //
                .setThreadName(threadName) //
                .setSource(location) //
                .setTimeMillis(timestamp) //
                .build();
    }
}

/**
 *
 */
abstract class ObjectPool<T> {
    private final Deque<T> pool = new ConcurrentLinkedDeque<>();

    public T borrowObject() {
        final T object = pool.poll();
        return object == null ? createObject() : object;
    }

    public void returnObject(final T object) {
        pool.add(object);
    }

    protected abstract T createObject();
}

/**
 *
 */
class StringBuilderPool extends ObjectPool<StringBuilder> {
    private final int initialSize;

    public StringBuilderPool(final int stringBuilderSize) {
        this.initialSize = stringBuilderSize;
    }

    @Override
    public void returnObject(final StringBuilder stringBuilder) {
        stringBuilder.setLength(0);
        super.returnObject(stringBuilder);
    }

    @Override
    protected StringBuilder createObject() {
        return new StringBuilder(initialSize);
    }
}
