/*
 * 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.ignite.internal.benchmarks.jmh.sequence;

import java.util.concurrent.ThreadLocalRandom;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteAtomicSequence;
import org.apache.ignite.Ignition;
import org.apache.ignite.configuration.AtomicConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.benchmarks.jmh.JmhAbstractBenchmark;
import org.apache.ignite.internal.benchmarks.jmh.runner.JmhIdeBenchmarkRunner;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
import org.openjdk.jmh.annotations.Benchmark;
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 static org.apache.ignite.internal.benchmarks.jmh.runner.JmhIdeBenchmarkRunner.createProperty;

/**
 * JMH benchmark for {@link IgniteAtomicSequence}.
 */
public class JmhSequenceBenchmark extends JmhAbstractBenchmark {

    /** Property: nodes count. */
    private static final String PROP_DATA_NODES = "ignite.jmh.sequence.dataNodes";

    /** Property: client mode flag. */
    private static final String PROP_CLIENT_MODE = "ignite.jmh.sequence.clientMode";

    /** Property: reservation batch size. */
    private static final String PROP_BATCH_SIZE = "ignite.jmh.sequence.batchSize";

    /** */
    @State(Scope.Benchmark)
    public static class SequenceState {
        /** IP finder shared across nodes. */
        private static final TcpDiscoveryVmIpFinder IP_FINDER = new TcpDiscoveryVmIpFinder(true);

        /** 1/10 of batchSize. */
        private int randomBound;

        /** */
        private IgniteAtomicSequence seq;

        /**
         * Setup.
         */
        @Setup
        public void setup() {
            Ignite node = Ignition.start(configuration("NODE_0"));

            int nodes = intProperty(PROP_DATA_NODES, 4);

            for (int i = 1; i < nodes; i++)
                Ignition.start(configuration("NODE_" + i));

            boolean isClient = booleanProperty(PROP_CLIENT_MODE);

            if (isClient) {
                IgniteConfiguration clientCfg = configuration("client");

                clientCfg.setClientMode(true);

                node = Ignition.start(clientCfg);
            }

            AtomicConfiguration acfg = new AtomicConfiguration();

            int batchSize = intProperty(PROP_BATCH_SIZE);

            randomBound = batchSize < 10 ? 1 : batchSize / 10;

            acfg.setAtomicSequenceReserveSize(batchSize);

            seq = node.atomicSequence("seq", acfg, 0, true);
        }

        /**
         * Create Ignite configuration.
         *
         * @param igniteInstanceName Ignite instance name.
         * @return Configuration.
         */
        private IgniteConfiguration configuration(String igniteInstanceName) {
            IgniteConfiguration cfg = new IgniteConfiguration();

            cfg.setIgniteInstanceName(igniteInstanceName);

            cfg.setLocalHost("127.0.0.1");

            TcpDiscoverySpi discoSpi = new TcpDiscoverySpi();

            discoSpi.setIpFinder(IP_FINDER);

            cfg.setDiscoverySpi(discoSpi);

            return cfg;
        }

        /**
         * Tear down routine.
         */
        @TearDown
        public void tearDown() {
            Ignition.stopAll(true);
        }
    }

    /**
     * Run benchmarks.
     *
     * @param args Arguments.
     * @throws Exception If failed.
     */
    public static void main(String[] args) throws Exception {
        run(false, 4, 10_000);
        run(false, 4, 100_000);

        run(true, 4, 10_000);
        run(true, 4, 100_000);
    }

    /**
     * @param client Client node.
     * @param dataNodes Number of data nodes.
     * @param batchSize Batch size.
     * @throws Exception If failed.
     */
    private static void run(boolean client, int dataNodes, int batchSize) throws Exception {
        String simpleClsName = JmhSequenceBenchmark.class.getSimpleName();

        String output = simpleClsName +
            "-" + (client ? "client" : "data") +
            "-" + dataNodes +
            "-" + batchSize;

        JmhIdeBenchmarkRunner.create()
            .forks(1)
            .threads(5)
            .warmupIterations(10)
            .measurementIterations(20)
            .output(output + ".jmh.log")
            .benchmarks(JmhSequenceBenchmark.class.getSimpleName())
            .jvmArguments(
                "-Xms4g",
                "-Xmx4g",
                createProperty(PROP_BATCH_SIZE, batchSize),
                createProperty(PROP_DATA_NODES, dataNodes),
                createProperty(PROP_CLIENT_MODE, client)
            )
            .run();
    }

    /**
     * Benchmark for {@link IgniteAtomicSequence#incrementAndGet} operation.
     *
     * @return Long new value.
     */
    @Benchmark
    public long incrementAndGet(SequenceState state) {
        return state.seq.incrementAndGet();
    }

    /**
     * Benchmark for {@link IgniteAtomicSequence#getAndIncrement()} operation.
     *
     * @return Long previous value.
     */
    @Benchmark
    public long getAndIncrement(SequenceState state) {
        return state.seq.getAndIncrement();
    }

    /**
     * Benchmark for {@link IgniteAtomicSequence#addAndGet(long)} operation.
     *
     * @return Long new value.
     */
    @Benchmark
    public long addAndGet(SequenceState state) {
        int key = ThreadLocalRandom.current().nextInt(state.randomBound) + 1;

        return state.seq.getAndAdd(key);
    }

    /**
     * Benchmark for {@link IgniteAtomicSequence#getAndAdd(long)} operation.
     *
     * @return Long previous value.
     */
    @Benchmark
    public long getAndAdd(SequenceState state) {
        int key = ThreadLocalRandom.current().nextInt(state.randomBound) + 1;

        return state.seq.getAndAdd(key);
    }
}
