/*******************************************************************************
 * Copyright (c) 2016 École Polytechnique de Montréal
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.tracecompass.lttng2.kernel.core.tests.perf.analysis.syscall;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.test.performance.Dimension;
import org.eclipse.test.performance.Performance;
import org.eclipse.test.performance.PerformanceMeter;
import org.eclipse.tracecompass.analysis.os.linux.core.tid.TidAnalysisModule;
import org.eclipse.tracecompass.internal.analysis.os.linux.core.latency.SystemCallLatencyAnalysis;
import org.eclipse.tracecompass.lttng2.kernel.core.trace.LttngKernelTrace;
import org.eclipse.tracecompass.segmentstore.core.BasicSegment;
import org.eclipse.tracecompass.segmentstore.core.ISegment;
import org.eclipse.tracecompass.segmentstore.core.ISegmentStore;
import org.eclipse.tracecompass.testtraces.ctf.CtfTestTrace;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfTraceException;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceOpenedSignal;
import org.eclipse.tracecompass.tmf.core.tests.shared.TmfTestHelper;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.eclipse.tracecompass.tmf.ctf.core.event.CtfTmfEvent;
import org.eclipse.tracecompass.tmf.ctf.core.tests.shared.CtfTmfTestTraceUtils;
import org.junit.Test;

import com.google.common.collect.Iterables;

/**
 * Benchmarks the system call latency analysis
 *
 * @author Geneviève Bastien
 */
public class SystemCallAnalysisUsageBenchmark {

    /**
     * Test test ID for the system call analysis benchmarks
     */
    public static final String TEST_ID = "org.eclipse.tracecompass#System Call Analysis#";
    private static final String TEST_ITERATE_CPU = "Iterate cpu (%s)";
    private static final String TEST_ITERATE_MEMORY = "Iterate memory (%s)";
    private static final String TEST_INTERSECTION_CPU = "Intersection cpu (%s)";
    private static final String TEST_INTERSECTION_MEMORY = "Intersection memory (%s)";

    private static final int LOOP_COUNT = 25;

    private static void deleteSupplementaryFiles(@NonNull ITmfTrace trace) {
        /*
         * Delete the supplementary files at the beginning and end of the benchmarks
         */
        File suppDir = new File(TmfTraceManager.getSupplementaryFileDir(trace));
        for (File file : suppDir.listFiles()) {
            file.delete();
        }
    }

    private static SystemCallLatencyAnalysis getModule(@NonNull CtfTestTrace testTrace, @NonNull LttngKernelTrace trace) {
        SystemCallLatencyAnalysis module = null;
        String path = CtfTmfTestTraceUtils.getTrace(testTrace).getPath();

        try {

            // Make sure the TID analysis has run on this trace
            trace.initTrace(null, path, CtfTmfEvent.class);
            deleteSupplementaryFiles(trace);

            trace.traceOpened(new TmfTraceOpenedSignal(trace, trace, null));
            IAnalysisModule tidModule = trace.getAnalysisModule(TidAnalysisModule.ID);
            assertNotNull(tidModule);
            tidModule.schedule();
            tidModule.waitForCompletion();

            /* Initialize the analysis module */
            module = new SystemCallLatencyAnalysis();
            module.setId("test");
            module.setTrace(trace);
            TmfTestHelper.executeAnalysis(module);
        } catch (TmfAnalysisException | TmfTraceException e) {
            fail(e.getMessage());
        }
        return module;
    }

    /**
     * Run the benchmark with "trace2"
     */
    @Test
    public void testTrace2() {
        runTest(CtfTestTrace.TRACE2, "Trace2");
    }

    /**
     * Run the benchmark with "many thread"
     */
    @Test
    public void testManyThreads() {
        runTest(CtfTestTrace.MANY_THREADS, "Many threads");
    }

    /**
     * Run the benchmark with "django httpd"
     */
    @Test
    public void testDjangoHttpd() {
        runTest(CtfTestTrace.DJANGO_HTTPD, "Django HTTPD");
    }

    private static void runTest(@NonNull CtfTestTrace testTrace, String traceName) {
        /* First, complete the analysis */
        LttngKernelTrace trace = new LttngKernelTrace();
        SystemCallLatencyAnalysis module = getModule(testTrace, trace);

        long segmentEnd = benchmarkIteration(traceName, module);
        benchmarkIntersection(traceName, module, trace.getStartTime().getValue(), segmentEnd);

        /*
         * Delete the supplementary files at the end of the benchmarks
         */
        deleteSupplementaryFiles(trace);

        module.dispose();
        trace.dispose();

        CtfTmfTestTraceUtils.dispose(testTrace);
    }

    /**
     * Benchmarks iterating through all the segments of the analysis
     */
    private static long benchmarkIteration(String testName, SystemCallLatencyAnalysis module) {

        Performance perf = Performance.getDefault();
        PerformanceMeter pmCpu = perf.createPerformanceMeter(TEST_ID + String.format(TEST_ITERATE_CPU, testName));
        perf.tagAsSummary(pmCpu, "Syscall " + String.format(TEST_ITERATE_CPU, testName), Dimension.CPU_TIME);

        PerformanceMeter pmMemory = perf.createPerformanceMeter(TEST_ID + String.format(TEST_ITERATE_MEMORY, testName));
        perf.tagAsSummary(pmMemory, "Syscall " + String.format(TEST_ITERATE_MEMORY, testName), Dimension.USED_JAVA_HEAP);

        ISegmentStore<@NonNull ISegment> ss = module.getSegmentStore();
        if (ss == null) {
            fail("The segment store is null");
            return -1;
        }

        long endTime = -1;

        /** Benchmark for CPU time */
        for (int i = 0; i < LOOP_COUNT; i++) {
            /* Make a full query at fixed intervals */
            pmCpu.start();
            for (ISegment segment : ss) {
                endTime = Math.max(endTime, segment.getEnd());
            }
            pmCpu.stop();
        }

        /** Benchmark for memory usage */
        for (int i = 0; i < LOOP_COUNT; i++) {
            /* Make a full query at fixed intervals */
            System.gc();
            pmMemory.start();
            for (ISegment segment : ss) {
                endTime = Math.max(endTime, segment.getEnd());
            }
            System.gc();
            pmMemory.stop();
        }

        pmCpu.commit();
        pmMemory.commit();
        return endTime;
    }

    /**
     * Benchmarks iterating through all the segments of the analysis
     */
    private static void benchmarkIntersection(String testName, SystemCallLatencyAnalysis module, long start, long end) {

        Performance perf = Performance.getDefault();
        PerformanceMeter pmCpu = perf.createPerformanceMeter(TEST_ID + String.format(TEST_INTERSECTION_CPU, testName));
        perf.tagAsSummary(pmCpu, "Syscall " + String.format(TEST_INTERSECTION_CPU, testName), Dimension.CPU_TIME);

        PerformanceMeter pmMemory = perf.createPerformanceMeter(TEST_ID + String.format(TEST_INTERSECTION_MEMORY, testName));
        perf.tagAsSummary(pmMemory, "Syscall " + String.format(TEST_INTERSECTION_MEMORY, testName), Dimension.USED_JAVA_HEAP);

        ISegmentStore<@NonNull ISegment> ss = module.getSegmentStore();
        if (ss == null) {
            fail("The segment store is null");
            return;
        }

        /** Benchmark for CPU time */
        for (int i = 0; i < LOOP_COUNT; i++) {
            /* Make a full query at fixed intervals */
            long intersectStart = start;
            long intersectEnd = end;
            pmCpu.start();
            for (int j = 0; j < 10; j++) {
                /**
                 * Iterate through the intersection -- this forces lazily evaluated iterables to
                 * do queries
                 */
                Iterables.getLast(ss.getIntersectingElements(intersectStart, intersectEnd), new BasicSegment(0l, 1l));
                long step = (long) ((intersectEnd - intersectStart) * 0.1);
                intersectStart += step;
                intersectEnd -= step;
            }
            pmCpu.stop();
        }

        /** Benchmark for memory usage */
        for (int i = 0; i < LOOP_COUNT; i++) {
            /* Make a full query at fixed intervals */
            List<Iterable<ISegment>> lists = new ArrayList<>();
            long intersectStart = start;
            long intersectEnd = end;
            System.gc();
            pmMemory.start();
            for (int j = 0; j < 5; j++) {
                Iterable<@NonNull ISegment> elements = ss.getIntersectingElements(intersectStart, intersectEnd);
                /**
                 * Iterate through the intersection -- this forces lazily evaluated iterables to
                 * do queries, and identify lingering data structures.
                 */
                Iterables.getLast(elements, new BasicSegment(0l, 1l));
                lists.add(elements);
                long step = (long) ((intersectEnd - intersectStart) * 0.2);
                intersectStart += step;
                intersectEnd -= step;
            }
            System.gc();
            pmMemory.stop();
        }

        pmCpu.commit();
        pmMemory.commit();
    }

}
