/*
 * 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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.ThreadContext;
import org.apache.logging.log4j.ThreadContextBenchmarkAccess;
import org.apache.logging.log4j.core.ContextDataInjector;
import org.apache.logging.log4j.core.config.Property;
import org.apache.logging.log4j.core.impl.ContextDataInjectorFactory;
import org.apache.logging.log4j.perf.nogc.OpenHashStringMap;
import org.apache.logging.log4j.spi.DefaultThreadContextMap;
import org.apache.logging.log4j.spi.GarbageFreeOpenHashMapThreadContextMap;
import org.apache.logging.log4j.spi.ThreadContextMap;
import org.apache.logging.log4j.util.SortedArrayStringMap;
import org.apache.logging.log4j.util.StringMap;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Warmup;

/**
 * Compares performance of ThreadContextMap implementations.
 */
// ============================== 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 ".*ThreadContextBench.*"
//
// four threads:
// java -jar log4j-perf/target/benchmarks.jar ".*ThreadContextBench.*" -f 1 -wi 10 -i 20 -tu ns -bm sample -t 4
//
// Usage help:
// java -jar log4j-perf/target/benchmarks.jar -help
//
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class ThreadContextBenchmark {
    private static final String DEFAULT_CONTEXT_MAP = "Default";
    private static final String NO_GC_OPENHASH_MAP = "NoGcOpenHash";
    private static final String NO_GC_ARRAY_MAP = "NoGcSortedArray";
    private static final Map<String, Class<? extends ThreadContextMap>> IMPLEMENTATIONS = new HashMap<>();

    static {
        IMPLEMENTATIONS.put(DEFAULT_CONTEXT_MAP, DefaultThreadContextMap.class);
        IMPLEMENTATIONS.put(NO_GC_OPENHASH_MAP, GarbageFreeOpenHashMapThreadContextMap.class);
        IMPLEMENTATIONS.put(
                NO_GC_ARRAY_MAP,
                GarbageFreeOpenHashMapThreadContextMap.SUPER); // GarbageFreeSortedArrayThreadContextMap.class);
    }

    @Param({"Default", "NoGcOpenHash", "NoGcSortedArray"})
    public String threadContextMapAlias;

    @Param({"5", "50", "500"})
    public int count;

    private final int KEY_LENGTH = 16;
    private String[] keys;
    private String[] values;
    private List<Property> propertyList;

    private ContextDataInjector injector;
    private StringMap reusableContextData;

    @Setup
    public void setup() {
        System.setProperty(
                "log4j2.threadContextMap",
                IMPLEMENTATIONS.get(threadContextMapAlias).getName());
        ThreadContextBenchmarkAccess.init();

        injector = ContextDataInjectorFactory.createInjector();
        System.out.println(threadContextMapAlias + ": Injector = " + injector);

        reusableContextData =
                threadContextMapAlias.contains("Array") ? new SortedArrayStringMap() : new OpenHashStringMap<>();

        keys = new String[count];
        values = new String[count];
        final Random r = new Random();
        for (int j = 0; j < keys.length; j++) {
            final char[] str = new char[KEY_LENGTH];
            for (int i = 0; i < str.length; i++) {
                str[i] = (char) r.nextInt();
            }
            keys[j] = new String(str);
            values[j] = new String(str);
        }
        final int PROPERTIES_COUNT = 5; // count
        propertyList = new ArrayList<>(PROPERTIES_COUNT);
        for (int j = 0; j < PROPERTIES_COUNT; j++) {
            final char[] str = new char[KEY_LENGTH];
            for (int i = 0; i < str.length; i++) {
                str[i] = (char) r.nextInt();
            }
            propertyList.add(Property.createProperty(new String(str), new String(str)));
        }

        clearAndPut(); // ensure ThreadContext contains values
    }

    @TearDown
    public void tearDown() {
        System.clearProperty("log4j2.threadContextMap");
        ThreadContextBenchmarkAccess.init();
    }

    public void clearAndPut() {
        ThreadContext.clearMap();
        for (int i = 0; i < count; i++) {
            ThreadContext.put(keys[i], values[i]);
        }
    }

    @Benchmark
    public Object get() {
        return ThreadContext.get(keys[count - 1]);
    }

    @Benchmark
    public void putAndRemove() {
        ThreadContext.put("someKey", "someValue");
        ThreadContext.remove("someKey");
    }

    @Benchmark
    public StringMap injectWithoutProperties() {
        reusableContextData.clear();
        return injector.injectContextData(null, reusableContextData);
    }

    @Benchmark
    public StringMap injectWithProperties() {
        reusableContextData.clear();
        return injector.injectContextData(propertyList, reusableContextData);
    }

    @Benchmark
    public Map<String, String> legacyInjectWithoutProperties() {
        return createMap(null);
    }

    @Benchmark
    public Map<String, String> legacyInjectWithProperties() {
        return createMap(propertyList);
    }

    // from Log4jLogEvent::createMap
    static Map<String, String> createMap(final List<Property> properties) {
        final Map<String, String> contextMap = ThreadContext.getImmutableContext();
        if (properties == null || properties.isEmpty()) {
            return contextMap; // may be ThreadContext.EMPTY_MAP but not null
        }
        final Map<String, String> map = new HashMap<>(contextMap);

        for (final Property prop : properties) {
            if (!map.containsKey(prop.getName())) {
                map.put(prop.getName(), prop.getValue());
            }
        }
        return Collections.unmodifiableMap(map);
    }
}
