<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/model/SingleProfileResult.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

import com.nikitakozlov.pury.result.ResultVisitor;

import java.util.List;

/**
 * Represent result for a single stage.
 */
public class SingleProfileResult implements ProfileResult {

    private final String mStageName;
    private final List<SingleProfileResult> mNestedResults;
    private final long mStartTime;
    private final long mExecTime;
    private final int mDepth;

    public SingleProfileResult(String stageName, long startTime, long execTime,
                               List<SingleProfileResult> nestedResults, int depth) {
        mStageName = stageName;
        mNestedResults = nestedResults;
        mStartTime = startTime;
        mExecTime = execTime;
        mDepth = depth;
    }

    /**
     * @return start time of a stage since start of the top most stage.
     */
    public long getStartTime() {
        return mStartTime;
    }

    /**
     * @return execution time of a stage.
     */
    public long getExecTime() {
        return mExecTime;
    }

    @Override
    public List<? extends ProfileResult> getNestedResults() {
        return mNestedResults;
    }

    @Override
    public void accept(ResultVisitor visitor) {
        visitor.visit(this);
    }
    @Override
    public String getStageName() {
        return mStageName;
    }

    @Override
    public int getDepth() {
        return mDepth;
    }
}


<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/SplashActivity.java<|end_filename|>
package com.nikitakozlov.pury_example;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.app.AppCompatActivity;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.annotations.StartProfiling;
import com.nikitakozlov.pury.annotations.StopProfiling;
import com.nikitakozlov.pury.Logger;
import com.nikitakozlov.pury_example.profilers.StartApp;

public class SplashActivity extends AppCompatActivity {

    @StartProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.TOP_STAGE,
            stageOrder = StartApp.TOP_STAGE_ORDER)
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
    }

    @StartProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.SPLASH_SCREEN,
            stageOrder = StartApp.SPLASH_SCREEN_ORDER)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        loadData();
        //This method does nothing. It is here to test no-op version.
        PuryNoOpTest.test();
    }
    @StartProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.SPLASH_LOAD_DATA,
            stageOrder = StartApp.SPLASH_LOAD_DATA_ORDER)
    private void loadData() {
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {

            @Override
            public void run() {
                onDataLoaded();
                startMainActivity();
            }
        }, 1000);
    }

    @StopProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.SPLASH_SCREEN)
    private void onDataLoaded() {

    }

    @StartProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.MAIN_ACTIVITY_LAUNCH,
            stageOrder = StartApp.MAIN_ACTIVITY_LAUNCH_ORDER)
    private void startMainActivity() {
        startActivity(new Intent(SplashActivity.this, MainActivity.class));
    }
}


<|start_filename|>pury-no-op/src/main/java/com/nikitakozlov/pury/result/model/AverageProfileResult.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

import com.nikitakozlov.pury.result.ResultVisitor;

import java.util.List;

/**
 * Represent result for multiple runs for a single stage.
 */
public class AverageProfileResult implements ProfileResult {

    public AverageProfileResult(String stageName, AverageTime startTime, AverageTime execTime,
                                List<AverageProfileResult> averageProfileResults, int depth) {
    }

    /**
     * @return Averaged start time since start of top most stage.
     */
    public AverageTime getStartTime() {
        return null;
    }

    /**
     * @return Averaged execution time.
     */
    public AverageTime getExecTime() {
        return null;
    }

    @Override
    public List<? extends ProfileResult> getNestedResults() {
        return null;
    }

    @Override
    public void accept(ResultVisitor visitor) {
    }

    @Override
    public String getStageName() {
        return null;
    }

    @Override
    public int getDepth() {
        return 0;
    }
}




<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/profile/ProfilerTest.java<|end_filename|>
package com.nikitakozlov.pury.profile;


import com.nikitakozlov.pury.Logger;
import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.result.ResultManager;
import com.nikitakozlov.pury.result.model.ProfileResult;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.util.Arrays;
import java.util.Collections;

import static com.nikitakozlov.pury.Pury.LOG_TAG;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class ProfilerTest {

    private static final String METHOD_ID = "method id";
    private static final int RUN_COUNTER_1 = 1;
    private static final int RUN_COUNTER_2 = 2;

    private static final String STAGE_0 = "Stage 0";
    private static final String STAGE_1 = "Stage 1";
    private static final String STAGE_2 = "Stage 2";
    private static final int STAGE_ORDER_START_ORDER = Profiler.START_ORDER;
    private static final int STAGE_ORDER_1 = 1;
    private static final int STAGE_ORDER_2 = 2;

    @Mock
    Profiler.Callback mCallback;

    @Mock
    ProfileResultProcessor mResultProcessor;

    @Mock
    Logger mLogger;

    @Mock
    ResultManager mResultManager;

    @Mock
    RunFactory mRunFactory;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void startStage_LogWarning_IfNoRunsAndStageOrderNotEqualStartOrder() {
        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_1, STAGE_ORDER_1);

        verify(mLogger).warning(eq(LOG_TAG), anyString());
        verify(mRunFactory, never()).startNewRun(anyString(), anyInt());
    }

    @Test
    public void startStage_StartsRun_IfNoRuns() {
        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);

        verify(mRunFactory).startNewRun(STAGE_0, STAGE_ORDER_START_ORDER);
    }

    @Test
    public void startStage_StartsSecondRun_IfFirstRunIsStopped() {
        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_2);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_0);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);

        verify(mRunFactory, times(2)).startNewRun(STAGE_0, STAGE_ORDER_START_ORDER);
    }

    @Test
    public void startStage_DontStartsSecondRun_IfFirstRunIsStoppedAndRunCounterIsOne() {
        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_0);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);

        verify(mRunFactory).startNewRun(STAGE_0, STAGE_ORDER_START_ORDER);
    }

    @Test
    public void startStage_StartsStage_OnSecondStartStage() {
        Run run = mock(Run.class);
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER)).thenReturn(run);

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.startStage(STAGE_1, STAGE_ORDER_1);

        verify(run).startStage(STAGE_1, STAGE_ORDER_1);
        verify(mLogger, never()).error(anyString(), anyString());
    }

    @Test
    public void startStage_LogsError_OnSecondStartStage_WhenRunReturnsError() {
        StageError stageError = mock(StageError.class);
        //Any type, shouldn't be null.
        when(stageError.getType()).thenReturn(StageError.Type.START_PARENT_STAGE_IS_STOPPED);
        Run run = mock(Run.class);
        when(run.startStage(STAGE_1, STAGE_ORDER_1)).thenReturn(stageError);
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER)).thenReturn(run);

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.startStage(STAGE_1, STAGE_ORDER_1);

        verify(mLogger).error(eq(LOG_TAG), anyString());
    }

    @Test
    public void startStage_LogsError_WhenNestedStageOrderIsTooSmall() {
        StageError stageError = mock(StageError.class);
        //Any type, shouldn't be null.
        when(stageError.getType()).thenReturn(StageError.Type.START_TO_SMALL_ORDER);
        Run run = mock(Run.class);
        when(run.startStage(STAGE_1, STAGE_ORDER_START_ORDER)).thenReturn(stageError);
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER)).thenReturn(run);

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.startStage(STAGE_1, STAGE_ORDER_START_ORDER);

        verify(mLogger).error(eq(LOG_TAG), anyString());
    }


    @Test
    public void stopStage_CallProcessorAndResultManager_WhenRunCounterIsOne() {
        Run run = spy(new Run(STAGE_0, STAGE_ORDER_START_ORDER));
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER)).thenReturn(run);
        ProfileResult result = mock(ProfileResult.class);
        when(mResultProcessor.process(Collections.singletonList(run))).thenReturn(result);

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_0);

        verify(mResultManager).dispatchResult(result, profilerId);
        verify(mLogger, never()).result(eq(LOG_TAG), anyString());
        verify(mCallback).onDone(profilerId);
    }

    @Test
    public void stopStage_CallProcessorAndResultManagerOnSecondCall_WhenRunCounterIsTwo() {
        Run run = spy(new Run(STAGE_0, STAGE_ORDER_START_ORDER));
        Run run1 = spy(new Run(STAGE_0, STAGE_ORDER_START_ORDER));
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER)).thenReturn(run, run1);
        ProfileResult result = mock(ProfileResult.class);
        when(mResultProcessor.process(Arrays.asList(run, run1))).thenReturn(result);

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_2);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_0);

        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_0);

        verify(mResultManager).dispatchResult(result, profilerId);
        verify(mLogger, never()).result(eq(LOG_TAG), anyString());
        verify(mCallback).onDone(profilerId);
    }

    @Test
    public void stopStage_CallLoggerError_WhenStageIsAlreadyStoppedAndRunCounterIsOne() {
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER))
                .thenReturn(new Run(STAGE_0, STAGE_ORDER_START_ORDER));

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_0);
        profiler.stopStage(STAGE_0);

        verify(mLogger).error(eq(LOG_TAG), anyString());
        verify(mResultManager).dispatchResult(Matchers.<ProfileResult>any(), eq(profilerId));
    }

    @Test
    public void stopStage_CallLoggerError_WhenStageIsAlreadyStoppedAndRunCounterIsTwo() {
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER))
                .thenReturn(new Run(STAGE_0, STAGE_ORDER_START_ORDER));

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_2);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_0);
        profiler.stopStage(STAGE_0);

        verify(mLogger).error(eq(LOG_TAG), anyString());
        verify(mLogger, never()).result(eq(LOG_TAG), anyString());
    }

    @Test
    public void stopStage_CallLoggerError_WhenStageNameIsWrong() {
        when(mRunFactory.startNewRun(STAGE_0, STAGE_ORDER_START_ORDER))
                .thenReturn(new Run(STAGE_0, STAGE_ORDER_START_ORDER));

        ProfilerId profilerId = new ProfilerId(METHOD_ID, RUN_COUNTER_1);
        Profiler profiler = new Profiler(profilerId, mCallback, mResultProcessor, mResultManager, mLogger, mRunFactory);
        profiler.startStage(STAGE_0, STAGE_ORDER_START_ORDER);
        profiler.stopStage(STAGE_1);

        verify(mLogger).error(eq(LOG_TAG), anyString());
    }
}

<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/Profiler.java<|end_filename|>
package com.nikitakozlov.pury.profile;


import com.nikitakozlov.pury.Logger;
import com.nikitakozlov.pury.result.ResultManager;
import com.nikitakozlov.pury.result.model.ProfileResult;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import static com.nikitakozlov.pury.Pury.LOG_TAG;

public class Profiler {

    static final int START_ORDER = 0;

    private final ProfilerId mProfilerId;
    private final int mRunsCounter;
    private final Callback mCallback;
    private final ProfileResultProcessor mResultProcessor;
    private final ResultManager mResultManager;
    private final Logger mLogger;
    private final RunFactory mRunFactory;
    private final List<Run> mRuns;
    private Run mActiveRun;
    private volatile int mFinishedRuns;

    Profiler(ProfilerId profilerId, Callback callback,
             ProfileResultProcessor resultProcessor, ResultManager resultManager, Logger logger, RunFactory runFactory) {
        mProfilerId = profilerId;
        mRunFactory = runFactory;
        mRunsCounter = profilerId.getRunsCounter();
        mResultProcessor = resultProcessor;
        mResultManager = resultManager;
        mLogger = logger;
        mRuns = new CopyOnWriteArrayList<>();
        mCallback = callback;
        mFinishedRuns = 0;
    }

    /**
     * @param stageName  Name of a stage to start. Used in results.
     * @param stageOrder Stage order must be bigger then order of current most nested active stage.
     *                   First profiling must starts with value 0.
     */
    public synchronized void startStage(String stageName, int stageOrder) {
        if (mActiveRun == null || mActiveRun.isStopped()) {
            if (mRuns.size() < mRunsCounter) {
                startRun(stageName, stageOrder);
            }
        } else if (mActiveRun != null && !mActiveRun.isStopped()) {
            logIfStageError(mActiveRun.startStage(stageName, stageOrder));
        }
    }

    private void startRun(String stageName, int stageOrder) {
        if (stageOrder != START_ORDER) {
            mLogger.warning(LOG_TAG, "Profiler Name = \"" + mProfilerId.getProfilerName() + "\". " +
                    "Can't start Run, stage order for stage \"" + stageName + "\" is wrong. " +
                    "Expected value: " + START_ORDER + ", actual value: " + stageOrder + ".");
            return;
        }
        mActiveRun = mRunFactory.startNewRun(stageName, stageOrder);
        mRuns.add(mActiveRun);
    }

    /**
     * @param stageName Name of stage to stop. Used in results.
     */
    public synchronized void stopStage(String stageName) {
        if (mActiveRun != null) {

            StageError stageError = mActiveRun.stopStage(stageName);

            if (stageError == null) {
                if (mActiveRun.isStopped()) {
                    mFinishedRuns++;
                    logIfFinished();
                }
            } else {
                logIfStageError(stageError);
            }
        }
    }

    private void logIfStageError(StageError stageError) {
        if (stageError != null && !stageError.isInternal()) {
            mLogger.error(LOG_TAG, "Profiler Name = \"" + mProfilerId.getProfilerName() + "\". " + StageErrorUtils.format(stageError));
        }
    }

    private void logIfFinished() {
        if (mRunsCounter == mFinishedRuns) {
            ProfileResult profileResult = mResultProcessor.process(mRuns);
            mResultManager.dispatchResult(profileResult, mProfilerId);
            mCallback.onDone(mProfilerId);
        }
    }

    interface Callback {
        void onDone(ProfilerId profilerId);
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/profile/ProfileResultProcessorTest.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import com.nikitakozlov.pury.result.model.AverageProfileResult;
import com.nikitakozlov.pury.result.model.AverageTime;
import com.nikitakozlov.pury.result.model.ProfileResult;
import com.nikitakozlov.pury.result.model.RootAverageProfileResult;
import com.nikitakozlov.pury.result.model.RootSingleProfileResult;
import com.nikitakozlov.pury.result.model.SingleProfileResult;

import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

public class ProfileResultProcessorTest {

    private static final long PAUSE_30 = 30L;
    private static final long PAUSE_50 = 50L;

    private static final String STAGE_NAME_0 = "stage_name_0";
    private static final String STAGE_NAME_1 = "stage_name_1";
    private static final String STAGE_NAME_2 = "stage_name_2";
    private static final int STAGE_ORDER_0 = 0;
    private static final int STAGE_ORDER_1 = 1;
    private static final int STAGE_ORDER_2 = 2;

    private ProfileResultProcessor mProcessor;
    private RunFactory mRunFactory;

    @Before
    public void setUp() {
        mProcessor = new ProfileResultProcessor();
        mRunFactory = new RunFactory();
    }

    @Test
    public void process_ReturnsNull_IfListIsEmpty() {
        assertNull(mProcessor.process(Collections.<Run>emptyList()));
    }

    @Test
    public void process_ReturnsRootSingleProfitResultWithProperValues_WhenListHasOneRun() throws InterruptedException {
        Run run = getRunWithoutNestedStages(PAUSE_30);
        ProfileResult result = mProcessor.process(Collections.singletonList(run));
        assertTrue(result instanceof RootSingleProfileResult);
        RootSingleProfileResult rootResult = (RootSingleProfileResult) result;
        assertEquals(run.getRootStage().getExecTime(), rootResult.getExecTime());
    }

    @Test
    public void process_ReturnsResultWithNestedStageWithStartTimeFromRunStart_WhenListHasOneRunWithOneNestedStage() throws InterruptedException {
        Run run = mRunFactory.startNewRun(STAGE_NAME_0, STAGE_ORDER_0);
        Thread.sleep(PAUSE_30);
        run.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        run.stopStage(STAGE_NAME_0);

        Stage rootStage = run.getRootStage();
        Stage nestedStage = rootStage.getStages().get(0);
        long relativeStartTime = nestedStage.getStartTime() - rootStage.getStartTime();

        RootSingleProfileResult result = (RootSingleProfileResult) mProcessor.process(
                Collections.singletonList(run));
        SingleProfileResult nestedResult = (SingleProfileResult) result.getNestedResults().get(0);
        assertEquals(relativeStartTime, nestedResult.getStartTime());
        assertEquals(nestedStage.getExecTime(), nestedResult.getExecTime());
    }

    @Test
    public void process_ReturnsAverageOfExecutionTimeForRootStages() throws InterruptedException {
        List<Run> runs = Arrays.asList(getRunWithoutNestedStages(PAUSE_30),
                getRunWithoutNestedStages(PAUSE_50));
        Stage rootStage0 = runs.get(0).getRootStage();
        Stage rootStage1 = runs.get(1).getRootStage();

        AverageTime averageExecTime = calculateAverageTimeOfTwo(rootStage0.getExecTime(),
                rootStage1.getExecTime());

        ProfileResult result = mProcessor.process(runs);
        assertTrue(result instanceof RootAverageProfileResult);
        RootAverageProfileResult rootResult = (RootAverageProfileResult) result;
        assertEqualAverageTimes(averageExecTime, rootResult.getExecTime());
    }

    @Test
    public void process_ReturnsAverageForNestedStages() throws InterruptedException {
        List<Run> runs = Arrays.asList(getRunWithNestedStage(PAUSE_30),
                getRunWithNestedStage(PAUSE_50));

        Stage rootStage0 = runs.get(0).getRootStage();
        Stage rootStage1 = runs.get(1).getRootStage();

        Stage nestedStage0 = rootStage0.getStages().get(0);
        Stage nestedStage1 = rootStage1.getStages().get(0);

        AverageTime averageStartTime = calculateAverageTimeOfTwo(
                nestedStage0.getStartTime() - rootStage0.getStartTime(),
                nestedStage1.getStartTime() - rootStage1.getStartTime());

        AverageTime averageExecTime = calculateAverageTimeOfTwo(nestedStage0.getExecTime(),
                nestedStage1.getExecTime());

        RootAverageProfileResult result = (RootAverageProfileResult) mProcessor.process(runs);
        AverageProfileResult nestedResult = (AverageProfileResult) result.getNestedResults().get(0);

        assertEqualAverageTimes(averageStartTime, nestedResult.getStartTime());
        assertEqualAverageTimes(averageExecTime, nestedResult.getExecTime());
    }

    @Test
    public void process_ReturnsAverageForNestedStages_WhenSomeScenariosAreCut() throws InterruptedException {
        List<Run> runs = Arrays.asList(getRunWithNestedStage(PAUSE_30),
                getRunWithNestedStage(PAUSE_50), getRunWithoutNestedStages(PAUSE_50));

        Stage rootStage0 = runs.get(0).getRootStage();
        Stage rootStage1 = runs.get(1).getRootStage();

        Stage nestedStage0 = rootStage0.getStages().get(0);
        Stage nestedStage1 = rootStage1.getStages().get(0);

        AverageTime averageStartTime = calculateAverageTimeOfTwo(
                nestedStage0.getStartTime() - rootStage0.getStartTime(),
                nestedStage1.getStartTime() - rootStage1.getStartTime());

        AverageTime averageExecTime = calculateAverageTimeOfTwo(nestedStage0.getExecTime(),
                nestedStage1.getExecTime());

        RootAverageProfileResult result = (RootAverageProfileResult) mProcessor.process(runs);
        AverageProfileResult nestedResult = (AverageProfileResult) result.getNestedResults().get(0);

        assertEqualAverageTimes(averageStartTime, nestedResult.getStartTime());
        assertEqualAverageTimes(averageExecTime, nestedResult.getExecTime());
    }

    private Run getRunWithoutNestedStages(long pause) throws InterruptedException {
        Run run = mRunFactory.startNewRun(STAGE_NAME_0, STAGE_ORDER_0);
        Thread.sleep(pause);
        run.stopStage(STAGE_NAME_0);
        return run;
    }

    private Run getRunWithNestedStage(long pause) throws InterruptedException {
        Run run = mRunFactory.startNewRun(STAGE_NAME_0, STAGE_ORDER_0);
        Thread.sleep(pause);
        run.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        run.stopStage(STAGE_NAME_0);
        return run;
    }

    private AverageTime calculateAverageTimeOfTwo(long time1, long time2) {
        long min = Math.min(time1, time2);
        long max = Math.max(time1, time2);
        double average = time1/2.0 + time2/2.0;
        return new AverageTime(average, min, max, 2);
    }

    private static void assertEqualAverageTimes(AverageTime expected, AverageTime actual) {
        assertEquals(expected.getAverageValue(), actual.getAverageValue(), 0.001d);
        assertEquals(expected.getMaxValue(), actual.getMaxValue());
        assertEquals(expected.getMinValue(), actual.getMinValue());
        assertEquals(expected.getMeasurementCounter(), expected.getMeasurementCounter());
    }
}

<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/MainActivity.java<|end_filename|>
package com.nikitakozlov.pury_example;

import android.content.Context;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import com.nikitakozlov.pury.annotations.MethodProfiling;
import com.nikitakozlov.pury.annotations.StopProfiling;
import com.nikitakozlov.pury_example.profilers.StartApp;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
    }

    @MethodProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.MAIN_ACTIVITY_CREATE,
            stageOrder = StartApp.MAIN_ACTIVITY_CREATE_ORDER)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("MainActivity", "onCreate");
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(MainActivity.this, ArticleListActivity.class));
            }
        });
    }

    @MethodProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.MAIN_ACTIVITY_START,
            stageOrder = StartApp.MAIN_ACTIVITY_START_ORDER)
    @Override
    protected void onStart() {
        super.onStart();
        try {
            Thread.sleep(30);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @StopProfiling(profilerName = StartApp.PROFILER_NAME, stageName = StartApp.TOP_STAGE)
    @Override
    protected void onResume() {
        super.onResume();
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/model/AverageTime.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

/**
 * Represents average time for given amount of measurements.
 */
public class AverageTime {


    private final double mAverageValue;
    private final long mMinValue;
    private final long mMaxValue;
    private final int mMeasurementCounter;

    public AverageTime(double averageValue, long minValue, long maxValue, int measurementCounter) {
        mAverageValue = averageValue;
        mMinValue = minValue;
        mMaxValue = maxValue;
        mMeasurementCounter = measurementCounter;
    }

    /**
      * @return average time.
     */
    public double getAverageValue() {
        return mAverageValue;
    }
    /**
     * @return min time from all measurements.
     */
    public long getMinValue() {
        return mMinValue;
    }

    /**
     * @return max time from all measurements.
     */
    public long getMaxValue() {
        return mMaxValue;
    }

    /**
     * @return amount of measurements.
     */
    public int getMeasurementCounter() {
        return mMeasurementCounter;
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/ProfilingManager.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.result.ResultManager;

import java.util.HashMap;
import java.util.Map;

public class ProfilingManager {

    private final ResultManager mResultManager;
    private final Map<ProfilerId, Profiler> mProfilers = new HashMap<>();
    private final ProfileResultProcessor mResultProcessor = new ProfileResultProcessor();
    private final RunFactory mRunFactory = new RunFactory();

    private final Profiler.Callback mProfilerCallback = new Profiler.Callback() {
        @Override
        public void onDone(ProfilerId profilerId) {
            removeProfiler(profilerId);
        }
    };

    public ProfilingManager(ResultManager resultManager) {
        mResultManager = resultManager;
    }

    /**
     *
     * @param profilerId ID that identifies a given Profiler.
     * @return {@link Profiler} that correspond to the given ProfilerId. If there is no
     *         corresponding Profiler yet, new instance will be created.
     */
    public synchronized Profiler getProfiler(ProfilerId profilerId) {
        if (mProfilers.containsKey(profilerId)) {
            return mProfilers.get(profilerId);
        }
        Profiler methodProfiler = new Profiler(profilerId, mProfilerCallback, mResultProcessor,
                mResultManager, Pury.getLogger(), mRunFactory);
        mProfilers.put(profilerId, methodProfiler);
        return methodProfiler;
    }

    private synchronized void removeProfiler(ProfilerId profilerId) {
        mProfilers.remove(profilerId);
    }

    public synchronized void clear() {
        mProfilers.clear();
    }
}


<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/ArticleDataSource.java<|end_filename|>
package com.nikitakozlov.pury_example;

import java.util.ArrayList;
import java.util.List;

public class ArticleDataSource {
    private static final String ARTICLE = "Article ";
    private static final int ARTICLES_PER_PAGE = 16;

    public List<String> getNextPage(int pageNumber) {
        List<String> articles = new ArrayList<>();

        for (int i = (pageNumber - 1) * ARTICLES_PER_PAGE; i < pageNumber * ARTICLES_PER_PAGE; i++) {
            articles.add(ARTICLE + i);
        }
        return articles;
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/StageErrorUtils.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public final class StageErrorUtils {
    private StageErrorUtils() {
    }

    public static String format(StageError error) {
        switch (error.getType()) {
            case START_TO_SMALL_ORDER:
                return "You are trying to start stage: \"" + error.getIntendedStageName() +
                        "\", but it's order value is smaller or equal to parent's stage has: " +
                        error.getIntendedStageOrder() + ". Parent stage: name = \"" +
                        error.getParentStageName() + "\", order value = " + error.getParentStageOrder() + ".";
            case STOP_NOT_STARTED_STAGE:
                return "You are trying to stop stage: \"" + error.getIntendedStageName() +
                        "\" with order value " + error.getIntendedStageOrder() +
                        ", but it wasn't started.";
            case STOPPED_ALREADY:
                return "You are trying to stop stage: \"" + error.getIntendedStageName() +
                        "\" with order value " + error.getIntendedStageOrder() +
                        ", but it is already stopped.";
            default:
                break;
        }
        return error.toString();
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/aspects/MethodProfilingAspectTest.java<|end_filename|>
package com.nikitakozlov.pury.aspects;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.PurySetter;
import com.nikitakozlov.pury.annotations.MethodProfiling;
import com.nikitakozlov.pury.annotations.MethodProfilings;
import com.nikitakozlov.pury.profile.Profiler;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.profile.ProfilingManager;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class MethodProfilingAspectTest {

    private static final int RUNS_COUNTER_5 = 5;
    private static final String PROFILER_NAME_1 = "profilerName 1";
    private static final int STAGE_ORDER_1 = 1;
    private static final String STAGE_NAME_1 = "stageName 1";


    private static final int RUNS_COUNTER_4 = 4;
    private static final String PROFILER_NAME_2 = "profilerName 2";
    private static final int STAGE_ORDER_2 = 2;
    private static final String STAGE_NAME_2 = "stageName 2";


    private static final int RUNS_COUNTER_3 = 3;
    private static final String PROFILER_NAME_3 = "profilerName 3";
    private static final int STAGE_ORDER_3 = 3;
    private static final String STAGE_NAME_3 = "stageName 3";

    private ProfilingManager profilingManager;
    private MethodProfilingAspect aspect;

    @Before
    public void setUp() {
        profilingManager = mock(ProfilingManager.class);
        PurySetter.setProfilingManager(profilingManager);
        aspect = new MethodProfilingAspect();
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromMethodProfilingAnnotationAndStartProfiler() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId))).thenReturn(profiler);

        aspect.weaveJoinPoint(mockProceedingJoinPoint("methodWithMethodProfilingAnnotation"));

        verify(profilingManager, times(2)).getProfiler(eq(profilerId));
        verify(profiler).startStage(STAGE_NAME_1, STAGE_ORDER_1);
        verify(profiler).stopStage(STAGE_NAME_1);
    }

    @Test
    public void weaveJoinPoint_CreatesDefaultStageNameAndStartProfiler_WhenMethodProfilingAnnotationHasNoStageName() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId))).thenReturn(profiler);

        String methodName = "methodWithMethodProfilingAnnotationWithoutStageName";
        String defaultStageName = this.getClass().getSimpleName() + "." + methodName;

        aspect.weaveJoinPoint(mockProceedingJoinPoint(methodName));

        verify(profilingManager, times(2)).getProfiler(eq(profilerId));
        verify(profiler).startStage(defaultStageName, STAGE_ORDER_1);
        verify(profiler).stopStage(defaultStageName);
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenMethodProfilingAnnotationIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        aspect.weaveJoinPoint(mockProceedingJoinPoint("methodWithDisabledMethodProfilingAnnotation"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenPuryIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        Pury.setEnabled(false);
        aspect.weaveJoinPoint(mockProceedingJoinPoint("methodWithMethodProfilingAnnotation"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromStartProfilingsAnnotationAndStartProfilers() throws Throwable {
        ProfilerId profilerId1 = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler1 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId1))).thenReturn(profiler1);

        ProfilerId profilerId2 = new ProfilerId(PROFILER_NAME_2, RUNS_COUNTER_4);
        Profiler profiler2 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId2))).thenReturn(profiler2);

        aspect.weaveJoinPoint(mockProceedingJoinPoint("methodWithMethodProfilingsAnnotation"));

        verify(profilingManager, times(2)).getProfiler(eq(profilerId1));
        verify(profiler1).startStage(STAGE_NAME_1, STAGE_ORDER_1);
        verify(profiler1).stopStage(STAGE_NAME_1);

        verify(profilingManager, times(2)).getProfiler(eq(profilerId2));
        verify(profiler2).startStage(STAGE_NAME_2, STAGE_ORDER_2);
        verify(profiler2).stopStage(STAGE_NAME_2);
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenMethodProfilingAnnotationsChildIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        aspect.weaveJoinPoint(mockProceedingJoinPoint("methodWithMethodProfilingsAnnotationWithDisabledChild"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromBothAnnotationsAndStartProfilers() throws Throwable {
        ProfilerId profilerId1 = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler1 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId1))).thenReturn(profiler1);

        ProfilerId profilerId2 = new ProfilerId(PROFILER_NAME_2, RUNS_COUNTER_4);
        Profiler profiler2 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId2))).thenReturn(profiler2);

        ProfilerId profilerId3 = new ProfilerId(PROFILER_NAME_3, RUNS_COUNTER_3);
        Profiler profiler3 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId3))).thenReturn(profiler3);

        aspect.weaveJoinPoint(mockProceedingJoinPoint("methodWithBothAnnotations"));

        verify(profilingManager, times(2)).getProfiler(eq(profilerId1));
        verify(profiler1).startStage(STAGE_NAME_1, STAGE_ORDER_1);
        verify(profiler1).stopStage(STAGE_NAME_1);

        verify(profilingManager, times(2)).getProfiler(eq(profilerId2));
        verify(profiler2).startStage(STAGE_NAME_2, STAGE_ORDER_2);
        verify(profiler2).stopStage(STAGE_NAME_2);

        verify(profilingManager, times(2)).getProfiler(eq(profilerId3));
        verify(profiler3).startStage(STAGE_NAME_3, STAGE_ORDER_3);
        verify(profiler3).stopStage(STAGE_NAME_3);
    }

    /**
     * This test is made just to remove point cut methods from test coverage diagram.
     */
    @Test
    public void dummyTest() {
        aspect.constructor();
        aspect.method();
        aspect.constructorWithMultipleAnnotations();
        aspect.methodWithMultipleAnnotations();
    }

    @After
    public void tearDown() {
        PurySetter.setProfilingManager(null);
        Pury.setEnabled(true);
    }

    private ProceedingJoinPoint mockProceedingJoinPoint(String methodName) throws NoSuchMethodException {
        ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
        MethodSignature methodSignature = mock(MethodSignature.class);
        when(methodSignature.getDeclaringType()).thenReturn(this.getClass());
        when(methodSignature.getName()).thenReturn(methodName);

        when(methodSignature.getMethod()).thenReturn(this.getClass().getDeclaredMethod(methodName));
        when(joinPoint.getSignature()).thenReturn(methodSignature);
        return joinPoint;
    }

    @MethodProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
            stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1)
    private void methodWithMethodProfilingAnnotation() {
    }

    @MethodProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
            stageOrder = STAGE_ORDER_1)
    private void methodWithMethodProfilingAnnotationWithoutStageName() {
    }

    @MethodProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
            stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1, enabled = false)
    private void methodWithDisabledMethodProfilingAnnotation() {
    }

    @MethodProfilings(value = {
            @MethodProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1),
            @MethodProfiling(runsCounter = RUNS_COUNTER_4, profilerName = PROFILER_NAME_2,
                    stageName = STAGE_NAME_2, stageOrder = STAGE_ORDER_2)
    })
    private void methodWithMethodProfilingsAnnotation() {
    }

    @MethodProfilings(value = {
            @MethodProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1, enabled = false)
    })
    private void methodWithMethodProfilingsAnnotationWithDisabledChild() {
    }

    @MethodProfilings(value = {
            @MethodProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1),
            @MethodProfiling(runsCounter = RUNS_COUNTER_4, profilerName = PROFILER_NAME_2,
                    stageName = STAGE_NAME_2, stageOrder = STAGE_ORDER_2)
    })
    @MethodProfiling(runsCounter = RUNS_COUNTER_3, profilerName = PROFILER_NAME_3,
            stageName = STAGE_NAME_3, stageOrder = STAGE_ORDER_3)
    private void methodWithBothAnnotations() {
    }
}

<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/profilers/StartApp.java<|end_filename|>
package com.nikitakozlov.pury_example.profilers;

public final class StartApp {
    public static final String PROFILER_NAME = "App Start";

    public static final String TOP_STAGE ="App Start";
    public static final int TOP_STAGE_ORDER = 0;

    public static final String SPLASH_SCREEN = "Splash Screen";
    public static final int SPLASH_SCREEN_ORDER = TOP_STAGE_ORDER + 1;

    public static final String SPLASH_LOAD_DATA = "Splash Load Data";
    public static final int SPLASH_LOAD_DATA_ORDER = SPLASH_SCREEN_ORDER + 1;

    public static final String MAIN_ACTIVITY_LAUNCH = "Main Activity Launch";
    public static final int MAIN_ACTIVITY_LAUNCH_ORDER = SPLASH_SCREEN_ORDER + 1;

    public static final String MAIN_ACTIVITY_CREATE = "onCreate()";
    public static final int MAIN_ACTIVITY_CREATE_ORDER = MAIN_ACTIVITY_LAUNCH_ORDER + 1;

    public static final String MAIN_ACTIVITY_START = "onStart()";
    public static final int MAIN_ACTIVITY_START_ORDER = MAIN_ACTIVITY_LAUNCH_ORDER + 1;
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/profile/StopWatchTest.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import org.junit.Test;

import static java.lang.Thread.sleep;
import static org.junit.Assert.*;

public class StopWatchTest {

    private static long ONE_HUNDERD_MILLIS = 100L;

    @Test
    public void getExecTimeInMillis_ReturnsTimeBiggerThenTimeBetweenStartAndStop() throws InterruptedException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        sleep(ONE_HUNDERD_MILLIS);
        stopWatch.stop();
        assertTrue(ONE_HUNDERD_MILLIS * 1000000 <= stopWatch.getExecTime());
    }

    @Test
    public void getStartTimeInMillis_ReturnsTimeBiggerThenCurrentTimeBeforeStart() throws InterruptedException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        long currentTime = System.nanoTime();
        assertTrue(currentTime >= stopWatch.getStartTime());
    }

    @Test
    public void isStopped_ReturnsTrue_AfterStop() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        stopWatch.stop();
        assertTrue(stopWatch.isStopped());
    }

    @Test
    public void isStopped_CantStart_AfterStop() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        long firstStartTime = stopWatch.getStartTime();
        stopWatch.stop();
        stopWatch.start();
        assertEquals(firstStartTime, stopWatch.getStartTime());
    }
    @Test
    public void isStopped_CantStopTwice() {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        stopWatch.stop();
        long firstStopTime = stopWatch.getExecTime();
        stopWatch.stop();
        assertEquals(firstStopTime, stopWatch.getExecTime());
    }
}

<|start_filename|>pury-no-op/src/main/java/com/nikitakozlov/pury/result/model/AverageTime.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

/**
 * Represents average time for given amount of measurements.
 */
public class AverageTime {

    public AverageTime(double averageValue, long minValue, long maxValue, int measurementCounter) {
    }

    /**
      * @return average time.
     */
    public double getAverageValue() {
        return 0;
    }
    /**
     * @return min time from all measurements.
     */
    public long getMinValue() {
        return 0;
    }

    /**
     * @return max time from all measurements.
     */
    public long getMaxValue() {
        return 0;
    }

    /**
     * @return amount of measurements.
     */
    public int getMeasurementCounter() {
        return 0;
    }

}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/aspects/StopProfilingAspect.java<|end_filename|>
package com.nikitakozlov.pury.aspects;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.annotations.StopProfiling;
import com.nikitakozlov.pury.annotations.StopProfilings;
import com.nikitakozlov.pury.profile.ProfilingManager;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.profile.StageId;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;

@Aspect
public class StopProfilingAspect {
    private static final String POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.StopProfiling * *(..))";

    private static final String POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.StopProfiling *.new(..))";


    private static final String GROUP_ANNOTATION_POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.StopProfilings * *(..))";

    private static final String GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.StopProfilings *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void method() {
    }

    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructor() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_METHOD)
    public void methodWithMultipleAnnotations() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR)
    public void constructorWithMultipleAnnotations() {
    }

    @After("constructor() || method() || methodWithMultipleAnnotations() || constructorWithMultipleAnnotations()")
    public void weaveJoinPoint(JoinPoint joinPoint) throws Throwable {
        if (!Pury.isEnabled()) {
            return;
        }

        ProfilingManager profilingManager = Pury.getProfilingManager();
        for (StageId stageId : getStageIds(joinPoint)) {
            profilingManager.getProfiler(stageId.getProfilerId())
                    .stopStage(stageId.getStageName());
        }

    }

    private List<StageId> getStageIds(JoinPoint joinPoint) {
        Annotation[] annotations =
                ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotations();
        List<StageId> stageIds = new ArrayList<>();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == StopProfiling.class) {
                StageId stageId = getStageId((StopProfiling) annotation);
                if (stageId != null) {
                    stageIds.add(stageId);
                }
            }
            if (annotation.annotationType() == StopProfilings.class) {
                for (StopProfiling stopProfiling : ((StopProfilings) annotation).value()) {
                    StageId stageId = getStageId(stopProfiling);
                    if (stageId != null) {
                        stageIds.add(stageId);
                    }
                }
            }
        }
        return stageIds;
    }

    private StageId getStageId(StopProfiling annotation) {
        if (!annotation.enabled()) {
            return null;
        }
        ProfilerId profilerId = new ProfilerId(annotation.profilerName(), annotation.runsCounter());
        return new StageId(profilerId, annotation.stageName());
    }
}


<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/ArticleListActivity.java<|end_filename|>
package com.nikitakozlov.pury_example;

import android.graphics.Rect;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;

import com.nikitakozlov.pury.annotations.MethodProfiling;
import com.nikitakozlov.pury.annotations.StartProfiling;
import com.nikitakozlov.pury.annotations.StopProfiling;
import com.nikitakozlov.pury_example.profilers.Pagination;

import java.util.List;
import java.util.Random;

public class ArticleListActivity extends AppCompatActivity {

    private static final int LOAD_PAGE_DELAY = 200;
    private static final int PAGE_PROCESS_DELAY = 50;
    private static final int DELAY_MAX_VARIATON = 50;

    private static final int VISIBLE_THRESHOLD = 8;
    private final static int ARTICLE_PADDING = 4;
    private final static int SPAN_COUNT = 2;

    private final Random random = new Random();
    private final ArticleDataSource mArticleDataSource = new ArticleDataSource();

    private final RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
        @Override
        public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
            if (mIsLoading) {
                return;
            }
            LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
            int visibleItemCount = layoutManager.getChildCount();
            int totalItemCount = layoutManager.getItemCount();
            int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();

            if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount - VISIBLE_THRESHOLD) {
                getNextPage();
            }
        }
    };

    private RecyclerView recyclerView;
    private ArticleAdapter mArticleAdapter;
    private volatile int mPage = 0;
    private boolean mIsLoading;
    private boolean mFinalize = true;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_article_list);
        initRecyclerView();
        getNextPage();
    }

    private void initRecyclerView() {
        recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        recyclerView.setHasFixedSize(true);
        recyclerView.setLayoutManager(new GridLayoutManager(this, SPAN_COUNT, LinearLayoutManager.VERTICAL, false));
        recyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
                super.getItemOffsets(outRect, view, parent, state);
                outRect.bottom = ARTICLE_PADDING;
                outRect.top = ARTICLE_PADDING;
                outRect.left = ARTICLE_PADDING;
                outRect.right = ARTICLE_PADDING;
            }
        });
        mArticleAdapter = new ArticleAdapter();
        recyclerView.setAdapter(mArticleAdapter);
        recyclerView.setOnScrollListener(scrollListener);
    }

    @StartProfiling(profilerName = Pagination.PROFILER_NAME, runsCounter = Pagination.RUN_COUNTER,
            stageName = Pagination.TOP_STAGE, stageOrder = Pagination.TOP_STAGE_ORDER)
    private void getNextPage() {

        if (mIsLoading) {
            return;
        }

        mIsLoading = true;
        new AsyncTask<Integer, Void, List<String>>() {

            @Override
            protected List<String> doInBackground(Integer... integers) {
                return processNextPage(loadNextPage(integers[0]));
            }

            @StopProfiling(profilerName = Pagination.PROFILER_NAME, runsCounter = Pagination.RUN_COUNTER,
                    stageName = Pagination.TOP_STAGE)
            @Override
            protected void onPostExecute(List<String> articles) {
                mArticleAdapter.addItems(articles);
                mPage++;
                mIsLoading = false;
            }
        }.execute(mPage + 1);
    }

    @MethodProfiling(profilerName = Pagination.PROFILER_NAME, runsCounter = Pagination.RUN_COUNTER,
            stageName = Pagination.LOAD_PAGE, stageOrder = Pagination.LOAD_PAGE_ORDER)
    private List<String> loadNextPage(Integer page) {
        try {
            Thread.sleep((long) (LOAD_PAGE_DELAY + random.nextFloat() * DELAY_MAX_VARIATON));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return mArticleDataSource.getNextPage(page);
    }

    @MethodProfiling(profilerName = Pagination.PROFILER_NAME, runsCounter = Pagination.RUN_COUNTER,
            stageName = Pagination.PROCESS_PAGE, stageOrder = Pagination.PROCESS_PAGE_ORDER)
    private List<String> processNextPage(final List<String> articles) {
        try {
            Thread.sleep((long) (PAGE_PROCESS_DELAY + random.nextFloat() * DELAY_MAX_VARIATON));

            //This is made to test when some runs has less stages then others
            if (mFinalize) {
                finalizePage();
                mFinalize = false;
            } else {
                mFinalize = true;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return articles;
    }

    @MethodProfiling(profilerName = Pagination.PROFILER_NAME, runsCounter = Pagination.RUN_COUNTER,
            stageName = Pagination.FINALIZE_PAGE, stageOrder = Pagination.FINALIZE_PAGE_ORDER)
    private void finalizePage() throws InterruptedException {
        Thread.sleep(20L);
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/aspects/StartProfilingAspect.java<|end_filename|>
package com.nikitakozlov.pury.aspects;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.annotations.StartProfiling;
import com.nikitakozlov.pury.annotations.StartProfilings;
import com.nikitakozlov.pury.profile.ProfilingManager;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.profile.StageId;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;

@Aspect
public class StartProfilingAspect {
    private static final String POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.StartProfiling * *(..))";

    private static final String POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.StartProfiling *.new(..))";


    private static final String GROUP_ANNOTATION_POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.StartProfilings * *(..))";


    private static final String GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.StartProfilings *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void method() {
    }

    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructor() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_METHOD)
    public void methodWithMultipleAnnotations() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR)
    public void constructorWithMultipleAnnotations() {
    }

    @Before("constructor() || method() || methodWithMultipleAnnotations() || constructorWithMultipleAnnotations()")
    public void weaveJoinPoint(JoinPoint joinPoint) throws Throwable {
        if (!Pury.isEnabled()) {
            return;
        }

        ProfilingManager profilingManager = Pury.getProfilingManager();
        for (StageId stageId : getStageIds(joinPoint)) {
            profilingManager.getProfiler(stageId.getProfilerId())
                    .startStage(stageId.getStageName(), stageId.getStageOrder());
        }
    }

    private List<StageId> getStageIds(JoinPoint joinPoint) {
        Annotation[] annotations =
                ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotations();
        List<StageId> stageIds = new ArrayList<>();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == StartProfiling.class) {
                StageId stageId = getStageId((StartProfiling) annotation);
                if (stageId != null) {
                    stageIds.add(stageId);
                }
            }
            if (annotation.annotationType() == StartProfilings.class) {
                for (StartProfiling startProfiling : ((StartProfilings) annotation).value()) {
                    StageId stageId = getStageId(startProfiling);
                    if (stageId != null) {
                        stageIds.add(stageId);
                    }
                }
            }
        }
        return stageIds;
    }

    private StageId getStageId(StartProfiling annotation) {
        if (!annotation.enabled()) {
            return null;
        }
        ProfilerId profilerId = new ProfilerId(annotation.profilerName(), annotation.runsCounter());
        return new StageId(profilerId, annotation.stageName(), annotation.stageOrder());
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/PuryTest.java<|end_filename|>
package com.nikitakozlov.pury;

import com.nikitakozlov.pury.profile.Profiler;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.profile.ProfilingManager;
import com.nikitakozlov.pury.result.model.ProfileResult;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Matchers;
import org.mockito.Mock;

import static org.junit.Assert.assertNotNull;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;

public class PuryTest {

    private static final int RUNS_COUNTER_1 = 1;
    private static final String PROFILER_NAME= "profilerName";
    private static final int STAGE_ORDER = 1;
    private static final int STAGE_ORDER_START_ORDER = 0;
    private static final String STAGE_NAME = "stageName";

    private static final String PLUGIN_KEY_1 = "key 1";
    private static final String PLUGIN_KEY_2 = "key 2";

    @Mock
    Plugin plugin1;

    @Mock
    Plugin plugin2;

    @Mock
    ProfilingManager mProfilingManager;

    @Mock
    Profiler mProfiler;

    @Before
    public void setUp() {
        initMocks(this);
    }

    @Test
    public void getProfilingManager_ReturnsNotNull() {
        assertNotNull(Pury.getProfilingManager());
    }

    @Test
    public void startProfiling_GetsProfilerAndStartsStage() {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME, RUNS_COUNTER_1);
        when(mProfilingManager.getProfiler(profilerId)).thenReturn(mProfiler);
        PurySetter.setProfilingManager(mProfilingManager);

        Pury.startProfiling(PROFILER_NAME, STAGE_NAME, STAGE_ORDER, RUNS_COUNTER_1);

        verify(mProfilingManager).getProfiler(profilerId);
        verify(mProfiler).startStage(STAGE_NAME, STAGE_ORDER);
    }

    @Test
    public void startProfiling_DoesNothing_WhenPuryIsDisabled() {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME, RUNS_COUNTER_1);
        PurySetter.setProfilingManager(mProfilingManager);

        Pury.setEnabled(false);
        Pury.startProfiling(PROFILER_NAME, STAGE_NAME, STAGE_ORDER, RUNS_COUNTER_1);

        verify(mProfilingManager, never()).getProfiler(profilerId);
    }

    @Test
    public void stopProfiling_GetsProfilerAndStopsStage() {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME, RUNS_COUNTER_1);
        when(mProfilingManager.getProfiler(profilerId)).thenReturn(mProfiler);
        PurySetter.setProfilingManager(mProfilingManager);

        Pury.stopProfiling(PROFILER_NAME, STAGE_NAME, RUNS_COUNTER_1);

        verify(mProfilingManager).getProfiler(profilerId);
        verify(mProfiler).stopStage(STAGE_NAME);
    }

    @Test
    public void stopProfiling_DoesNothing_WhenPuryIsDisabled() {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME, RUNS_COUNTER_1);
        PurySetter.setProfilingManager(mProfilingManager);

        Pury.setEnabled(false);
        Pury.stopProfiling(PROFILER_NAME, STAGE_NAME, RUNS_COUNTER_1);

        verify(mProfilingManager, never()).getProfiler(profilerId);
    }

    //First integration test for result handlers
    @Test
    public void pury_ShouldCallAllPlugins() {
        Pury.addPlugin(PLUGIN_KEY_1, plugin1);
        Pury.addPlugin(PLUGIN_KEY_2, plugin2);

        ProfilerId profilerId = new ProfilerId(PROFILER_NAME, RUNS_COUNTER_1);
        Pury.startProfiling(PROFILER_NAME, STAGE_NAME, STAGE_ORDER_START_ORDER, RUNS_COUNTER_1);
        Pury.stopProfiling(PROFILER_NAME, STAGE_NAME, RUNS_COUNTER_1);

        verify(plugin1).handleResult(Matchers.<ProfileResult>any(), eq(profilerId));
        verify(plugin2).handleResult(Matchers.<ProfileResult>any(), eq(profilerId));
    }

    //Second integration test for result handlers
    @Test
    public void pury_ShouldNotCallRemovedPlugin() {
        Pury.addPlugin(PLUGIN_KEY_1, plugin1);
        Pury.addPlugin(PLUGIN_KEY_2, plugin2);
        Pury.removePlugin(PLUGIN_KEY_1);

        ProfilerId profilerId = new ProfilerId(PROFILER_NAME, RUNS_COUNTER_1);
        Pury.startProfiling(PROFILER_NAME, STAGE_NAME, STAGE_ORDER_START_ORDER, RUNS_COUNTER_1);
        Pury.stopProfiling(PROFILER_NAME, STAGE_NAME, RUNS_COUNTER_1);

        verify(plugin1, never()).handleResult(Matchers.<ProfileResult>any(), eq(profilerId));
        verify(plugin2).handleResult(Matchers.<ProfileResult>any(), eq(profilerId));
    }

    @After
    public void tearDown() {
        PurySetter.setProfilingManager(null);
        Pury.setEnabled(true);
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/StageId.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public class StageId {
    private final ProfilerId profilerId;
    private final String stageName;
    private final int stageOrder;

    public StageId(ProfilerId profilerId, String stageName) {
        this(profilerId, stageName, 0);
    }

    public StageId(ProfilerId profilerId, String stageName, int stageOrder) {
        this.profilerId = profilerId;
        this.stageName = stageName;
        this.stageOrder = stageOrder;
    }

    public ProfilerId getProfilerId() {
        return profilerId;
    }

    public String getStageName() {
        return stageName;
    }

    public int getStageOrder() {
        return stageOrder;
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/LoggerPluginTest.java<|end_filename|>
package com.nikitakozlov.pury;

import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.result.model.AverageProfileResult;
import com.nikitakozlov.pury.result.model.AverageTime;
import com.nikitakozlov.pury.result.model.RootAverageProfileResult;
import com.nikitakozlov.pury.result.model.RootSingleProfileResult;
import com.nikitakozlov.pury.result.model.SingleProfileResult;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;

import java.util.Arrays;
import java.util.Collections;

import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;

public class LoggerPluginTest {

    @Mock
    Logger mLogger;

    @Before
    public void setUp() {
        initMocks(this);
        Pury.setLogger(mLogger);
    }

    @Test
    public void handleResult_ShouldLogProperly_WhenRunCounterIsOne() {
        String expectedResult = "Profiling results for App Start:\n" +
                "App Start --> 0ms\n" +
                "  Splash Screen --> 9ms\n" +
                "  Splash Screen <-- 1043ms, execution = 1034ms\n" +
                "  Main Activity Launch --> 1044ms\n" +
                "    onCreate() --> 1082ms\n" +
                "    onCreate() <-- 1149ms, execution = 67ms\n" +
                "  Main Activity Launch <-- 1241ms, execution = 197ms\n" +
                "App Start <-- 1242ms";
        ProfilerId profilerId = new ProfilerId("App Start", 1);

        SingleProfileResult splashScreenProfileResult = new SingleProfileResult("Splash Screen", 9000000L,
                1034000000L, Collections.<SingleProfileResult>emptyList(), 1);

        SingleProfileResult onCreateProfileResult = new SingleProfileResult("onCreate()", 1082000000L,
                67000000L, Collections.<SingleProfileResult>emptyList(), 2);
        SingleProfileResult mainActivityLaunchProfileResult = new SingleProfileResult("Main Activity Launch", 1044000000L,
                197000000L, Collections.singletonList(onCreateProfileResult), 1);

        RootSingleProfileResult appStartProfileResult = new RootSingleProfileResult("App Start",
                1242000000, Arrays.asList(splashScreenProfileResult, mainActivityLaunchProfileResult));

        LoggerPlugin loggerPlugin = new LoggerPlugin();

        loggerPlugin.handleResult(appStartProfileResult, profilerId);

        verify(mLogger).result(Pury.LOG_TAG, expectedResult);
    }

    @Test
    public void handleResult_ShouldNotLogProfileName_WhenProfileNameIsEmpty() {
        String expectedResult = "Profiling results:\n" +
                "App Start --> 0ms\n" +
                "App Start <-- 1242ms";
        ProfilerId profilerId = new ProfilerId("", 1);

        RootSingleProfileResult appStartProfileResult = new RootSingleProfileResult("App Start",
                1242000000, Collections.<SingleProfileResult>emptyList());

        LoggerPlugin loggerPlugin = new LoggerPlugin();

        loggerPlugin.handleResult(appStartProfileResult, profilerId);

        verify(mLogger).result(Pury.LOG_TAG, expectedResult);
    }


    @Test
    public void handleResult_ShouldLogProperly_WhenRunCounterIsFive() {
        String expectedResult = "Profiling results for Pagination:\n" +
                "Get Next Page --> 0ms\n" +
                "  Load --> avg = 1.08ms, min = 0ms, max = 2ms, for 5 runs\n" +
                "    ApiCall --> avg = 1.10ms, min = 0ms, max = 2ms, for 5 runs\n" +
                "    ApiCall <-- avg = 160.05ms, min = 130ms, max = 200ms, for 5 runs\n" +
                "  Load <-- avg = 260.97ms, min = 241ms, max = 285ms, for 5 runs\n" +
                "Get Next Page <-- avg = 380.11ms, min = 347ms, max = 406ms, for 5 runs";

        ProfilerId profilerId = new ProfilerId("Pagination", 5);

        AverageTime apiCallStartAvgTime = new AverageTime(1100000, 0, 2000000L, 5);
        AverageTime apiCallExecAvgTime = new AverageTime(160050000, 130000000L, 200000000L, 5);
        AverageProfileResult apiCallProfileResult = new AverageProfileResult("ApiCall",
                apiCallStartAvgTime, apiCallExecAvgTime, Collections.<AverageProfileResult>emptyList(), 2);

        AverageTime loadStartAvgTime = new AverageTime(1080000, 0, 2000000L, 5);
        AverageTime loadExecAvgTime = new AverageTime(260970000, 241000000L, 285000000L, 5);
        AverageProfileResult loadProfileResult = new AverageProfileResult("Load", loadStartAvgTime,
                loadExecAvgTime, Collections.singletonList(apiCallProfileResult), 1);

        AverageTime getNextPageAvgTime = new AverageTime(380110000, 347000000L, 406000000L, 5);
        RootAverageProfileResult rootAverageProfileResult = new RootAverageProfileResult("Get Next Page",
                getNextPageAvgTime, Collections.singletonList(loadProfileResult));

        LoggerPlugin loggerPlugin = new LoggerPlugin();

        loggerPlugin.handleResult(rootAverageProfileResult, profilerId);

        verify(mLogger).result(Pury.LOG_TAG, expectedResult);
    }

    @After
    public void tearDown() {
        Pury.setLogger(null);
    }

}

<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/StopWatch.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public class StopWatch {

    private long startTimeStamp;
    private long execTime;
    private boolean isStopped;

    public void start() {
        if (!isStopped) {
            startTimeStamp = System.nanoTime();
        }
    }

    public void stop() {
        if (!isStopped) {
            execTime = System.nanoTime() - startTimeStamp;
            isStopped = true;
        }
    }

    public boolean isStopped() {
        return isStopped;
    }

    public long getExecTime() {
        return execTime;
    }

    public long getStartTime() {
        return startTimeStamp;
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/result/ResultManagerTest.java<|end_filename|>
package com.nikitakozlov.pury.result;

import com.nikitakozlov.pury.Plugin;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.result.model.ProfileResult;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks;

public class ResultManagerTest {

    private static final String KEY_1 = "key 1";
    private static final String KEY_2 = "key 2";

    @Mock
    Plugin plugin1;

    @Mock
    Plugin plugin2;

    @Before
    public void setUp() {
        initMocks(this);
    }

    @Test
    public void dispatchResult_ShouldCallAllPlugins() {
        ProfilerId profilerId = mock(ProfilerId.class);
        ResultManager resultManager = new ResultManager();
        resultManager.addPlugin(KEY_1, plugin1);
        resultManager.addPlugin(KEY_2, plugin2);

        ProfileResult result = mock(ProfileResult.class);
        resultManager.dispatchResult(result, profilerId);
        verify(plugin1).handleResult(result, profilerId);
        verify(plugin2).handleResult(result, profilerId);
    }

    @Test
    public void dispatchResult_ShouldNotCallRemovedPlugin() {
        ProfilerId profilerId = mock(ProfilerId.class);
        ResultManager resultManager = new ResultManager();
        resultManager.addPlugin(KEY_1, plugin1);
        resultManager.addPlugin(KEY_2, plugin2);
        resultManager.removePlugin(KEY_1);

        ProfileResult result = mock(ProfileResult.class);
        resultManager.dispatchResult(result, profilerId);
        verify(plugin1, never()).handleResult(result, profilerId);
        verify(plugin2).handleResult(result, profilerId);
    }
}

<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/DefaultLogger.java<|end_filename|>
package com.nikitakozlov.pury;

import android.util.Log;

public class DefaultLogger implements Logger {
    @Override
    public void result(String tag, String message) {
        Log.d(tag, message);
    }

    @Override
    public void warning(String tag, String message) {
        Log.w(tag, message);
    }

    @Override
    public void error(String tag, String message) {
        Log.e(tag, message);
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/model/ProfileResult.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

import com.nikitakozlov.pury.result.ResultVisitor;

import java.util.List;

/**
 * Represents profiling result of a single stage.
 */
public interface ProfileResult {

    /**
     * Results preserve nesting structure of stages. So that if a given stage has two nested stages,
     * than corresponding result also has two nested results.
     * @return list of nested results.
     */
    List<? extends ProfileResult> getNestedResults();

    /**
     * @return name of a corresponding stage.
     */
    String getStageName();

    /**
     * @return level in the hierarchy of nested stages.
     */
    int getDepth();

    /**
     * To simplify result processing all results make use of Visitor Pattern.
     */
    void accept(ResultVisitor visitor);
}


<|start_filename|>pury-no-op/src/main/java/com/nikitakozlov/pury/result/model/SingleProfileResult.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

import com.nikitakozlov.pury.result.ResultVisitor;

import java.util.Collections;
import java.util.List;

/**
 * Represent result for a single stage.
 */
public class SingleProfileResult implements ProfileResult {

    public SingleProfileResult(String mStageName, long startTime, long execTime,
                               List<SingleProfileResult> nestedResults, int depth) {
    }

    /**
     * @return start time of a stage since start of the top most stage.
     */
    public long getStartTime() {
        return 0;
    }

    /**
     * @return execution time of a stage.
     */
    public long getExecTime() {
        return 0;
    }

    @Override
    public List<? extends ProfileResult> getNestedResults() {
        return null;
    }

    @Override
    public void accept(ResultVisitor visitor) {
    }
    @Override
    public String getStageName() {
        return null;
    }

    @Override
    public int getDepth() {
        return 0;
    }
}


<|start_filename|>pury-no-op/src/main/java/com/nikitakozlov/pury/profile/ProfilingManager.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public class ProfilingManager {

    /**
     *
     * @param profilerId ID that identifies a given Profiler.
     * @return {@link Profiler} that correspond to the given ProfilerId. If there is no
     *         corresponding Profiler yet, new instance will be created.
     */
    public synchronized Profiler getProfiler(ProfilerId profilerId) {
        return null;
    }

    public synchronized void clear() {
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/model/AverageProfileResult.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

import com.nikitakozlov.pury.result.ResultVisitor;

import java.util.List;

/**
 * Represent result for multiple runs for a single stage.
 */
public class AverageProfileResult implements ProfileResult {

    private final String mStageName;
    private final AverageTime mStartTime;
    private final AverageTime mExecTime;
    private final List<AverageProfileResult> mAverageProfileResults;
    private final int mDepth;

    public AverageProfileResult(String stageName, AverageTime startTime, AverageTime execTime,
                                List<AverageProfileResult> averageProfileResults, int depth) {
        mStageName = stageName;
        mStartTime = startTime;
        mExecTime = execTime;
        mAverageProfileResults = averageProfileResults;
        mDepth = depth;
    }

    /**
     * @return Averaged start time since start of top most stage.
     */
    public AverageTime getStartTime() {
        return mStartTime;
    }

    /**
     * @return Averaged execution time.
     */
    public AverageTime getExecTime() {
        return mExecTime;
    }

    @Override
    public List<? extends ProfileResult> getNestedResults() {
        return mAverageProfileResults;
    }

    @Override
    public void accept(ResultVisitor visitor) {
        visitor.visit(this);
    }

    @Override
    public String getStageName() {
        return mStageName;
    }

    @Override
    public int getDepth() {
        return mDepth;
    }
}




<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/Run.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public class Run {
    private final Stage mRootStage;

    public Run(String rootStageName, int rootStageOrder) {
        mRootStage = new Stage(rootStageName, rootStageOrder);
        mRootStage.start();
    }

    public StageError startStage(String stageName, int stageOrder) {
        return mRootStage.startStage(stageName, stageOrder);
    }

    public StageError stopStage(String stageName) {
         return mRootStage.stop(stageName);
    }

    public boolean isStopped() {
        return mRootStage.isStopped();
    }

    public Stage getRootStage() {
        return mRootStage;
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/profile/ProfilingManagerTest.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import com.nikitakozlov.pury.Pury;

import org.junit.After;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;

public class ProfilingManagerTest {

    private static final String STAGE_NAME_0 = "stage_name_0";
    private static final int STAGE_ORDER_0 = 0;
    private static final String METHOD_ID_1 = "method id 1";
    private static final String METHOD_ID_2 = "method id 2";
    private static final int RUN_COUNT_1 = 1;
    private static final int RUN_COUNT_2 = 2;

    @Test
    public void getProfiler_ReturnsProfiler() {
        ProfilingManager profilingManager = Pury.getProfilingManager();
        assertNotNull(profilingManager.getProfiler(new ProfilerId(METHOD_ID_1, RUN_COUNT_1)));
    }

    @Test
    public void getProfiler_ReturnsDifferentProfilersForDifferentIds() {
        ProfilingManager profilingManager = Pury.getProfilingManager();
        ProfilerId id1 = new ProfilerId(METHOD_ID_1, RUN_COUNT_1);
        ProfilerId id2 = new ProfilerId(METHOD_ID_2, RUN_COUNT_2);
        Profiler profiler1 = profilingManager.getProfiler(id1);
        Profiler profiler2 = profilingManager.getProfiler(id2);
        assertNotEquals(profiler1, profiler2);
    }

    @Test
    public void getProfiler_ReturnsSameProfilerForSameId() {
        ProfilingManager profilingManager = Pury.getProfilingManager();
        ProfilerId id = new ProfilerId(METHOD_ID_1, RUN_COUNT_1);
        Profiler profiler = profilingManager.getProfiler(id);
        assertEquals(profiler, profilingManager.getProfiler(id));
    }

    @Test
    public void getProfiler_ReturnsDifferentProfilersForSameId_IfFirstProfilerIsDone() {
        ProfilingManager profilingManager = Pury.getProfilingManager();
        ProfilerId id = new ProfilerId(METHOD_ID_1, RUN_COUNT_1);
        Profiler profiler = profilingManager.getProfiler(id);
        profiler.startStage(STAGE_NAME_0, STAGE_ORDER_0);
        profiler.stopStage(STAGE_NAME_0);
        Profiler profiler2 = profilingManager.getProfiler(id);
        assertNotEquals(profiler, profiler2);
    }

    @Test
    public void clear_RemovedAllProfilers() {
        ProfilerId id = new ProfilerId(METHOD_ID_1, RUN_COUNT_1);
        ProfilingManager profilingManager = Pury.getProfilingManager();
        Profiler profiler = profilingManager.getProfiler(id);
        profilingManager.clear();
        assertNotEquals(profiler, profilingManager.getProfiler(id));
    }

}

<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/LoggerPlugin.java<|end_filename|>
package com.nikitakozlov.pury;

import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.result.ResultVisitor;
import com.nikitakozlov.pury.result.model.AverageProfileResult;
import com.nikitakozlov.pury.result.model.AverageTime;
import com.nikitakozlov.pury.result.model.ProfileResult;
import com.nikitakozlov.pury.result.model.RootAverageProfileResult;
import com.nikitakozlov.pury.result.model.RootSingleProfileResult;
import com.nikitakozlov.pury.result.model.SingleProfileResult;

import java.util.Locale;

import static com.nikitakozlov.pury.Pury.LOG_TAG;

public class LoggerPlugin implements Plugin {

    private static final String RIGHT_ARROW = " --> ";
    private static final String LEFT_ARROW = " <-- ";
    private static final String MS = "ms";
    private static final String EXEC_TIME = "execution = ";
    private static final int MS_TO_NS = 1000000;
    private static final String DEPTH_PREFIX = "  ";

    private static final String AVERAGE = "avg = ";
    private static final String MIN = "min = ";
    private static final String MAX = "max = ";
    private static final String FOR = "for ";
    private static final String RUNS = " runs";

    @Override
    public void handleResult(ProfileResult result, ProfilerId profilerId) {
        Visitor visitor = new Visitor();
        result.accept(visitor);

        StringBuilder logMessage = new StringBuilder("Profiling results");
        String methodId = profilerId.getProfilerName();
        if (!methodId.isEmpty()) {
            logMessage.append(" for ")
                    .append(methodId);
        }
        logMessage.append(":\n")
                .append(visitor.getLogMessage());
        Pury.getLogger().result(LOG_TAG, logMessage.toString());
    }


    private static class Visitor implements ResultVisitor {

        private final StringBuilder logMessageBuilder = new StringBuilder();

        @Override
        public void visit(AverageProfileResult averageProfileResult) {

            appendFullDepthPrefix(logMessageBuilder, averageProfileResult.getDepth());
            logMessageBuilder.append(averageProfileResult.getStageName())
                    .append(RIGHT_ARROW);
            averageTimeToString(averageProfileResult.getStartTime(), logMessageBuilder);
            logMessageBuilder.append("\n");
            for (ProfileResult result : averageProfileResult.getNestedResults()) {
                result.accept(this);
                logMessageBuilder.append("\n");
            }
            appendFullDepthPrefix(logMessageBuilder, averageProfileResult.getDepth());
            logMessageBuilder.append(averageProfileResult.getStageName())
                    .append(LEFT_ARROW);
            averageTimeToString(averageProfileResult.getExecTime(), logMessageBuilder);
        }

        @Override
        public void visit(RootAverageProfileResult rootAverageProfileResult) {
            logMessageBuilder.append(rootAverageProfileResult.getStageName())
                    .append(" --> 0ms\n");
            for (ProfileResult result : rootAverageProfileResult.getNestedResults()) {
                result.accept(this);
                logMessageBuilder.append("\n");
            }
            logMessageBuilder.append(rootAverageProfileResult.getStageName())
                    .append(LEFT_ARROW);
            averageTimeToString(rootAverageProfileResult.getExecTime(), logMessageBuilder);
        }

        @Override
        public void visit(RootSingleProfileResult rootSingleProfileResult) {

            logMessageBuilder.append(rootSingleProfileResult.getStageName())
                    .append(" --> 0ms\n");
            for (ProfileResult result : rootSingleProfileResult.getNestedResults()) {
                result.accept(this);
                logMessageBuilder.append("\n");
            }
            logMessageBuilder.append(rootSingleProfileResult.getStageName())
                    .append(LEFT_ARROW)
                    .append(rootSingleProfileResult.getExecTime() / MS_TO_NS)
                    .append(MS);
        }

        @Override
        public void visit(SingleProfileResult singleProfileResult) {

            appendFullDepthPrefix(logMessageBuilder, singleProfileResult.getDepth());
            logMessageBuilder.append(singleProfileResult.getStageName())
                    .append(RIGHT_ARROW)
                    .append(singleProfileResult.getStartTime() / MS_TO_NS)
                    .append("ms\n");
            for (ProfileResult result : singleProfileResult.getNestedResults()) {
                result.accept(this);
                logMessageBuilder.append("\n");
            }
            appendFullDepthPrefix(logMessageBuilder, singleProfileResult.getDepth());
            logMessageBuilder.append(singleProfileResult.getStageName())
                    .append(LEFT_ARROW)
                    .append((singleProfileResult.getStartTime() + singleProfileResult.getExecTime()) / MS_TO_NS)
                    .append("ms, ")
                    .append(EXEC_TIME)
                    .append(singleProfileResult.getExecTime() / MS_TO_NS)
                    .append(MS);
        }

        String getLogMessage() {
            return logMessageBuilder.toString();
        }

        private void appendFullDepthPrefix(StringBuilder sb, int depth) {
            for (int i = 0; i < depth; i++) {
                sb.append(DEPTH_PREFIX);
            }
        }

        private void averageTimeToString(AverageTime time, StringBuilder stringBuilder) {
            stringBuilder.append(AVERAGE)
                    .append(String.format(Locale.US, "%.2f", time.getAverageValue() / MS_TO_NS))
                    .append(MS)
                    .append(", ")

                    .append(MIN)
                    .append(time.getMinValue() / MS_TO_NS)
                    .append(MS)
                    .append(", ")

                    .append(MAX)
                    .append(time.getMaxValue() / MS_TO_NS)
                    .append(MS)
                    .append(", ")

                    .append(FOR)
                    .append(time.getMeasurementCounter())
                    .append(RUNS);
        }
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/Pury.java<|end_filename|>
package com.nikitakozlov.pury;

import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.profile.ProfilingManager;
import com.nikitakozlov.pury.result.ResultManager;

/**
 * Main interface for interacting with Pury.
 */
public final class Pury {

    public static final String LOG_TAG = "Pury";
    public static final String LOGGER_PLUGIN = "LoggerPlugin";

    private static volatile Logger sLogger;
    private static volatile boolean sEnabled = true;

    private static final ResultManager sResultManager;
    private static volatile ProfilingManager sProfilingManager;

    static {
        sResultManager = new ResultManager();
        sResultManager.addPlugin(LOGGER_PLUGIN, new LoggerPlugin());
        sProfilingManager = new ProfilingManager(sResultManager);
    }

    /**
     * Set logger for Pury to use. Default Plugin will also use it for output.
     * @param logger Logger to set.
     */
    public static void setLogger(Logger logger) {
        sLogger = logger;
    }

    /**
     * @return Logger that was set previously. Or a default one if no logger was set.
     */
    public synchronized static Logger getLogger() {
        if (sLogger == null) {
            sLogger = new DefaultLogger();
        }
        return sLogger;
    }

    /**
     * @return true if Pury is enabled, false otherwise.
     */
    public static boolean isEnabled() {
        return sEnabled;
    }

    /**
     * @param enabled pass false if you want to disable Pury.
     */
    public synchronized static void setEnabled(boolean enabled) {
        if (!enabled) {
            sProfilingManager.clear();
        }
        sEnabled = enabled;
    }

    /**
     *
     * @param profilerName used to identify profiler. Used in results.
     * @param stageName Name of a stage to start. Used in results.
     * @param stageOrder Stage order must be bigger then order of current most nested active stage.
     *                   First profiling must starts with value 0.
     * @param runsCounter used to identify profiler. Amount of runs to average.
     *                    Result will be available only after all runs are stopped.
     */
    public static void startProfiling(String profilerName, String stageName, int stageOrder,
                                      int runsCounter) {
        if (isEnabled()) {
            ProfilerId profilerId = new ProfilerId(profilerName, runsCounter);
            sProfilingManager.getProfiler(profilerId).startStage(stageName, stageOrder);
        }
    }

    /**
     *
     * @param profilerName used to identify profiler. Used in results.
     * @param stageName  Name of a stage to stop. Used in results.
     * @param runsCounter used to identify profiler. Amount of runs to average.
     *                    Result will be available only after all runs are stopped.
     */
    public static void stopProfiling(String profilerName, String stageName, int runsCounter) {
        if (isEnabled()) {
            ProfilerId profilerId = new ProfilerId(profilerName, runsCounter);
            sProfilingManager.getProfiler(profilerId).stopStage(stageName);
        }
    }

    /**
     * Adding custom Plugins allows to customize result processing. For example instead of
     * logging result send it somewhere, or save it in a permanent storage. Default
     * {@link Plugin} logs result into {@link Logger}.
     * @param key that identify Plugin
     * @param plugin Plugin to process result
     */
    public static void addPlugin(String key, Plugin plugin) {
        sResultManager.addPlugin(key, plugin);
    }

    /**
     * Remove {@link Plugin} by key. To remove default result handler use {@link #LOGGER_PLUGIN}
     * @param key that identify Plugin to remove.
     */
    public static void removePlugin(String key) {
        sResultManager.removePlugin(key);
    }

    /**
     * {@link #startProfiling(String, String, int, int)} and {@link #stopProfiling(String, String, int)}
     * creates {@link ProfilerId} on every call. To avoid creating same ProfilerId multiple times,
     * it is possible to call ProfilingManager directly.
     * @return {@link ProfilingManager}
     */
    public static ProfilingManager getProfilingManager() {
        return sProfilingManager;
    }

    static void setProfilingManager(ProfilingManager profilingManager) {
        if (profilingManager == null) {
            sProfilingManager = new ProfilingManager(sResultManager);
        } else {
            sProfilingManager = profilingManager;
        }
    }

}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/Plugin.java<|end_filename|>
package com.nikitakozlov.pury;

import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.result.model.ProfileResult;

/**
 * Plugin allows to customize result processing. For example instead of logging result send
 * it somewhere, or save it in a permanent storage.
 */
public interface Plugin {

    /**
     * Hook method that will be called once result is ready.
     * @param result Result of profiling
     * @param profilerId profiler identifier.
     */
    void handleResult(ProfileResult result, ProfilerId profilerId);

}


<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/ArticleAdapter.java<|end_filename|>
package com.nikitakozlov.pury_example;

import android.graphics.Color;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.zip.Inflater;

class ArticleAdapter extends RecyclerView.Adapter<ArticleAdapter.ArticleViewHolder> {

    private final List<String> articles = new ArrayList<>();
    private final Random random = new Random();

    @Override
    public ArticleViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ArticleViewHolder(LayoutInflater.from(parent.getContext())
                .inflate(R.layout.article_item, parent, false));
    }

    @Override
    public void onBindViewHolder(ArticleViewHolder holder, int position) {
        holder.itemView.setBackgroundColor(Color.argb(255, random.nextInt(256), random.nextInt(256),
                random.nextInt(256)));
        holder.text.setText(articles.get(position));
    }

    @Override
    public int getItemCount() {
        return articles.size();
    }

    public void addItems(List<String> newArticles) {
        articles.addAll(newArticles);
        notifyItemRangeInserted(articles.size(), newArticles.size());
    }

    class ArticleViewHolder extends RecyclerView.ViewHolder {

        private TextView text;

        public ArticleViewHolder(View itemView) {
            super(itemView);
            text = (TextView) itemView.findViewById(R.id.text);
        }
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/model/RootAverageProfileResult.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

import com.nikitakozlov.pury.result.ResultVisitor;

import java.util.List;

/**
 * Represent result for multiple runs for top most stage. Compare to {@link AverageProfileResult},
 * it doesn't have Start Time because it is always zero.
 */
public class RootAverageProfileResult implements ProfileResult {

    private final String mStageName;
    private final AverageTime mExecTime;
    private final List<AverageProfileResult> mAverageProfileResults;

    public RootAverageProfileResult(String stageName, AverageTime execTime,
                                    List<AverageProfileResult> averageProfileResults) {
        mStageName = stageName;
        mExecTime = execTime;
        mAverageProfileResults = averageProfileResults;
    }

    /**
     * @return Averaged execution time.
     */
    public AverageTime getExecTime() {
        return mExecTime;
    }

    @Override
    public List<? extends ProfileResult> getNestedResults() {
        return mAverageProfileResults;
    }

    @Override
    public void accept(ResultVisitor visitor) {
        visitor.visit(this);
    }


    @Override
    public String getStageName() {
        return mStageName;
    }

    @Override
    public int getDepth() {
        return 0;
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/model/RootSingleProfileResult.java<|end_filename|>
package com.nikitakozlov.pury.result.model;

import com.nikitakozlov.pury.result.ResultVisitor;

import java.util.List;

/**
 * Represent result for a top most stage. Start time is always zero.
 */
public class RootSingleProfileResult extends SingleProfileResult {

    public RootSingleProfileResult(String stageName, long execTime, List<SingleProfileResult> nestedResults) {
        super(stageName, 0L, execTime, nestedResults, 0);
    }

    @Override
    public void accept(ResultVisitor visitor) {
        visitor.visit(this);
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/Stage.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Stage {
    private final String mName;
    private final int mOrder;
    private final StopWatch mStopWatch;
    private boolean mIsStarted;
    private boolean mIsStopped;
    private Stage mActiveNestedStage;
    private final List<Stage> mStages;

    public Stage(String name, int order) {
        this(name, order, new StopWatch());
    }

    Stage(String name, int order, StopWatch stopWatch) {
        mName = name;
        mOrder = order;
        mStopWatch = stopWatch;
        mStages = new CopyOnWriteArrayList<>();
    }

    public String getName() {
        return mName;
    }

    public int getOrder() {
        return mOrder;
    }


    /**
     * Start this stage
     * @return true if start was successful
     */
    public boolean start() {
        if (!mIsStarted && !mIsStopped) {
            mStopWatch.start();
            mIsStarted = true;
            return true;
        }
        return false;
    }


    public StageError startStage(String stageName, int stageOrder) {
        if (mOrder >= stageOrder) {
            return StageError.createError(stageName, stageOrder, this, StageError.Type.START_TO_SMALL_ORDER);
        }

        if (mIsStopped) {
            return StageError.createError(stageName, stageOrder, this, StageError.Type.START_PARENT_STAGE_IS_STOPPED);
        }

        if (!mIsStarted) {
            return StageError.createError(stageName, stageOrder, this, StageError.Type.START_PARENT_STAGE_NOT_STARTED);
        }

        if (mActiveNestedStage == null || mActiveNestedStage.mIsStopped) {
            startNewNestedStage(stageName, stageOrder);
            return null;
        } else {
            return mActiveNestedStage.startStage(stageName, stageOrder);
        }
    }

    private void startNewNestedStage(String stageName, int stageOrder) {
        Stage stage = new Stage(stageName, stageOrder);
        stage.start();
        mActiveNestedStage = stage;
        mStages.add(stage);
    }

    public StageError stop(String stageName) {
        if (mName.equals(stageName)) {
            if (!mIsStopped) {
                mStopWatch.stop();
                mIsStopped = true;
                mIsStarted = false;
                if (mActiveNestedStage != null) {
                    mActiveNestedStage.stop(mActiveNestedStage.getName());
                }
            } else {
                return StageError.createError(stageName, this, StageError.Type.STOPPED_ALREADY);
            }
        } else if (mActiveNestedStage != null) {
            return mActiveNestedStage.stop(stageName);
        } else {
            return StageError.createError(stageName, this, StageError.Type.STOP_NOT_STARTED_STAGE);
        }

        return null;
    }

    public boolean isStopped() {
        return mIsStopped;
    }

    boolean isStarted() {
        return mIsStarted;
    }

    public long getStartTime() {
        return mStopWatch.getStartTime();
    }
    public long getExecTime() {
        return mStopWatch.getExecTime();
    }

    public List<Stage> getStages() {
        return mStages;
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/profile/StageTest.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import org.junit.Test;

import static android.test.MoreAsserts.assertEmpty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class StageTest {

    private static final String STAGE_NAME_0 = "stage_name_0";
    private static final String STAGE_NAME_1 = "stage_name_1";
    private static final String STAGE_NAME_2 = "stage_name_2";
    private static final int STAGE_ORDER_0 = 0;
    private static final int STAGE_ORDER_1 = 1;
    private static final int STAGE_ORDER_2 = 2;

    @Test
    public void start_CallsStartOnStopWatch() {
        StopWatch stopWatch = mock(StopWatch.class);
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0, stopWatch);
        assertTrue(stage.start());
        verify(stopWatch).start();
    }

    @Test
    public void start_DontCallsSecondTimeStartOnStopWatch() {
        StopWatch stopWatch = mock(StopWatch.class);
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0, stopWatch);
        assertTrue(stage.start());
        assertFalse(stage.start());
        verify(stopWatch).start();
    }

    @Test
    public void start_MoveStageIntoStartedState() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        assertTrue(stage.isStarted());
    }

    @Test
    public void start_DontCallsSecondTimeStartOnStopWatch_WhenStopIsCalled() {
        StopWatch stopWatch = mock(StopWatch.class);
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0, stopWatch);
        assertTrue(stage.start());
        stage.stop(STAGE_NAME_0);
        assertFalse(stage.start());
        verify(stopWatch).start();
    }

    @Test
    public void stop_CallsStopOnStopWatch() {
        StopWatch stopWatch = mock(StopWatch.class);
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0, stopWatch);
        stage.start();
        assertNull(stage.stop(STAGE_NAME_0));
        verify(stopWatch).stop();
    }

    @Test
    public void stop_MoveStageIntoStoppedState() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        assertNull(stage.stop(STAGE_NAME_0));
        assertFalse(stage.isStarted());
        assertTrue(stage.isStopped());
    }

    @Test
    public void stop_StopOnlyNestedStage_IfNameEqualToNestedStagesName() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        assertNull(stage.stop(STAGE_NAME_1));
        assertTrue(stage.getStages().get(0).isStopped());
        assertFalse(stage.isStopped());
    }

    @Test
    public void stop_StopNestedStage_IfNameEqualToCurrentStagesName() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        assertNull(stage.stop(STAGE_NAME_0));
        assertTrue(stage.getStages().get(0).isStopped());
    }

    @Test
    public void stop_ReturnsError_IfAlreadyStopped() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.stop(STAGE_NAME_0);
        assertEquals(StageError.Type.STOPPED_ALREADY, stage.stop(STAGE_NAME_0).getType());
    }

    @Test
    public void stop_ReturnsError_IfNestedStageAlreadyStopped() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        stage.stop(STAGE_NAME_1);
        assertEquals(StageError.Type.STOPPED_ALREADY, stage.stop(STAGE_NAME_1).getType());
    }

    @Test
    public void stop_ReturnsError_IfStageNameDoestMatch() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        assertEquals(StageError.Type.STOP_NOT_STARTED_STAGE, stage.stop(STAGE_NAME_1).getType());
    }

    @Test
    public void stop_ReturnsError_IfStageNameDoestMatchWholeHierarchy() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        assertEquals(StageError.Type.STOP_NOT_STARTED_STAGE, stage.stop(STAGE_NAME_2).getType());
    }

    @Test
    public void startStage_ShouldNotStartNextStage_WhenStageOrderIsEqualToProvided() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        assertEquals(StageError.Type.START_TO_SMALL_ORDER,
                stage.startStage(STAGE_NAME_1, STAGE_ORDER_0).getType());
        assertEmpty(stage.getStages());
    }

    @Test
    public void startStage_ShouldNotStartNextStage_WhenStageOrderIsBiggerThenProvided() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_1);
        stage.start();
        assertEquals(StageError.Type.START_TO_SMALL_ORDER,
                stage.startStage(STAGE_NAME_1, STAGE_ORDER_0).getType());
        assertEmpty(stage.getStages());
    }

    @Test
    public void startStage_ShouldNotStartNextStage_IfCurrentStageWasNotStarted() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        assertEquals(StageError.Type.START_PARENT_STAGE_NOT_STARTED,
                stage.startStage(STAGE_NAME_1, STAGE_ORDER_1).getType());
        assertEmpty(stage.getStages());
    }

    @Test
    public void startStage_ShouldNotStartNextStage_IfCurrentStageWasStopped() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.stop(STAGE_NAME_0);
        assertEquals(StageError.Type.START_PARENT_STAGE_IS_STOPPED,
                stage.startStage(STAGE_NAME_1, STAGE_ORDER_1).getType());
        assertEmpty(stage.getStages());
    }

    @Test
    public void startStage_ShouldStartNextStage() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        assertNull(stage.startStage(STAGE_NAME_1, STAGE_ORDER_1));
        assertTrue(stage.getStages().get(0).isStarted());
    }

    @Test
    public void startStage_ShouldStartNextStage_IfPreviousIsStopped() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        stage.stop(STAGE_NAME_1);
        stage.startStage(STAGE_NAME_2, STAGE_ORDER_2);
        assertTrue(stage.getStages().get(1).isStarted());
    }

    @Test
    public void startStage_ShouldStartNextStageInNestedStage_IfPreviousIsNotStopped() {
        Stage stage = new Stage(STAGE_NAME_0, STAGE_ORDER_0);
        stage.start();
        stage.startStage(STAGE_NAME_1, STAGE_ORDER_1);
        stage.startStage(STAGE_NAME_2, STAGE_ORDER_2);
        Stage nestedStage = stage.getStages().get(0);
        assertTrue(nestedStage.getStages().get(0).isStarted());
    }
}

<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/profilers/Pagination.java<|end_filename|>
package com.nikitakozlov.pury_example.profilers;

public class Pagination {
    public static final String PROFILER_NAME = "Pagination";
    public static final int RUN_COUNTER = 2;

    public static final String TOP_STAGE ="Get Next Page";
    public static final int TOP_STAGE_ORDER = 0;

    public static final String LOAD_PAGE = "Load";
    public static final int LOAD_PAGE_ORDER = TOP_STAGE_ORDER + 1;

    public static final String PROCESS_PAGE = "Process";
    public static final int PROCESS_PAGE_ORDER = LOAD_PAGE_ORDER + 1;

    public static final String FINALIZE_PAGE = "Finalize";
    public static final int FINALIZE_PAGE_ORDER = PROCESS_PAGE_ORDER + 1;
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/ResultManager.java<|end_filename|>
package com.nikitakozlov.pury.result;

import com.nikitakozlov.pury.Plugin;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.result.model.ProfileResult;

import java.util.HashMap;
import java.util.Map;

public class ResultManager {
    private final Map<String, Plugin> plugins = new HashMap<>();

    public void dispatchResult(ProfileResult result, ProfilerId profilerId) {
        for (String key : plugins.keySet()) {
            plugins.get(key).handleResult(result, profilerId);
        }
    }

    public void addPlugin(String key, Plugin plugin) {
        plugins.put(key, plugin);
    }

    public void removePlugin(String key) {
        plugins.remove(key);
    }
}


<|start_filename|>example/src/main/java/com/nikitakozlov/pury_example/PuryNoOpTest.java<|end_filename|>
package com.nikitakozlov.pury_example;

import com.nikitakozlov.pury.Logger;
import com.nikitakozlov.pury.Plugin;
import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.result.model.ProfileResult;

public class PuryNoOpTest {

    public static void test() {
        String testNoopProfilerName = "Test noop";

        Pury.getLogger();
        Pury.isEnabled();
        Pury.setEnabled(true);
        Pury.startProfiling(testNoopProfilerName, "", 0, 1);
        Pury.stopProfiling(testNoopProfilerName, "", 1);
        Pury.setLogger(new Logger() {
            @Override
            public void result(String s, String s1) {

            }

            @Override
            public void warning(String s, String s1) {

            }

            @Override
            public void error(String s, String s1) {

            }
        });
        Pury.setLogger(null);

        testPlugins();
    }

    private static void testPlugins() {
        String key = "key";
        Pury.addPlugin(key, new Plugin() {
            @Override
            public void handleResult(ProfileResult result, ProfilerId profilerId) {

            }
        });
        Pury.removePlugin(key);
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/aspects/MethodProfilingAspect.java<|end_filename|>
package com.nikitakozlov.pury.aspects;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.annotations.MethodProfiling;
import com.nikitakozlov.pury.annotations.MethodProfilings;
import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.profile.ProfilingManager;
import com.nikitakozlov.pury.profile.StageId;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Aspect
public class MethodProfilingAspect {
    private static final String POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfiling * *(..))";

    private static final String POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfiling *.new(..))";


    private static final String GROUP_ANNOTATION_POINTCUT_METHOD =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfilings * *(..))";

    private static final String GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR =
            "execution(@com.nikitakozlov.pury.annotations.MethodProfilings *.new(..))";

    @Pointcut(POINTCUT_METHOD)
    public void method() {
    }

    @Pointcut(POINTCUT_CONSTRUCTOR)
    public void constructor() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_METHOD)
    public void methodWithMultipleAnnotations() {
    }

    @Pointcut(GROUP_ANNOTATION_POINTCUT_CONSTRUCTOR)
    public void constructorWithMultipleAnnotations() {
    }

    @Around("constructor() || method() || methodWithMultipleAnnotations() || constructorWithMultipleAnnotations()")
    public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        ProfilingManager profilingManager = Pury.getProfilingManager();

        List<StageId> stageIds = getStageIds(joinPoint);

        for (StageId stageId : stageIds) {
            profilingManager.getProfiler(stageId.getProfilerId())
                    .startStage(stageId.getStageName(), stageId.getStageOrder());
        }

        Object result = joinPoint.proceed();

        for (StageId stageId : stageIds) {
            profilingManager.getProfiler(stageId.getProfilerId())
                    .stopStage(stageId.getStageName());
        }

        return result;
    }

    private List<StageId> getStageIds(ProceedingJoinPoint joinPoint) {
        if (!Pury.isEnabled()) {
            return Collections.emptyList();
        }

        Annotation[] annotations =
                ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotations();
        List<StageId> stageIds = new ArrayList<>();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == MethodProfiling.class) {
                StageId stageId = getStageId((MethodProfiling) annotation, joinPoint);
                if (stageId != null) {
                    stageIds.add(stageId);
                }
            }
            if (annotation.annotationType() == MethodProfilings.class) {
                for (MethodProfiling methodProfiling : ((MethodProfilings) annotation).value()) {
                    StageId stageId = getStageId(methodProfiling, joinPoint);
                    if (stageId != null) {
                        stageIds.add(stageId);
                    }
                }
            }
        }
        return stageIds;
    }

    private StageId getStageId(MethodProfiling annotation, ProceedingJoinPoint joinPoint) {
        if (!annotation.enabled()) {
            return null;
        }
        ProfilerId profilerId = new ProfilerId(annotation.profilerName(), annotation.runsCounter());
        String stageName = annotation.stageName();
        if (stageName.isEmpty()) {

            CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();

            String className = codeSignature.getDeclaringType().getSimpleName();
            String methodName = codeSignature.getName();

            stageName = className + "." + methodName;
        }

        return new StageId(profilerId, stageName, annotation.stageOrder());
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/aspects/StartProfilingAspectTest.java<|end_filename|>
package com.nikitakozlov.pury.aspects;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.PurySetter;
import com.nikitakozlov.pury.annotations.StartProfiling;
import com.nikitakozlov.pury.annotations.StartProfilings;
import com.nikitakozlov.pury.profile.Profiler;
import com.nikitakozlov.pury.profile.ProfilingManager;
import com.nikitakozlov.pury.profile.ProfilerId;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class StartProfilingAspectTest {

    private static final int RUNS_COUNTER_5 = 5;
    private static final String PROFILER_NAME_1 = "profilerName 1";
    private static final int STAGE_ORDER_1 = 1;
    private static final String STAGE_NAME_1 = "stageName 1";


    private static final int RUNS_COUNTER_4 = 4;
    private static final String PROFILER_NAME_2 = "profilerName 2";
    private static final int STAGE_ORDER_2 = 2;
    private static final String STAGE_NAME_2 = "stageName 2";


    private static final int RUNS_COUNTER_3 = 3;
    private static final String PROFILER_NAME_3 = "profilerName 3";
    private static final int STAGE_ORDER_3 = 3;
    private static final String STAGE_NAME_3 = "stageName 3";

    private ProfilingManager profilingManager;
    private StartProfilingAspect aspect;

    @Before
    public void setUp() {
        profilingManager = mock(ProfilingManager.class);
        PurySetter.setProfilingManager(profilingManager);
        aspect = new StartProfilingAspect();
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromStartProfilingAnnotationAndStartProfiler() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId))).thenReturn(profiler);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithStartProfilingAnnotation"));

        verify(profilingManager).getProfiler(eq(profilerId));
        verify(profiler).startStage(STAGE_NAME_1, STAGE_ORDER_1);
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenStartProfilingAnnotationIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithDisabledStartProfilingAnnotation"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenPuryIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        Pury.setEnabled(false);
        aspect.weaveJoinPoint(mockJoinPoint("methodWithStartProfilingAnnotation"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromStartProfilingsAnnotationAndStartProfilers() throws Throwable {
        ProfilerId profilerId1 = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler1 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId1))).thenReturn(profiler1);

        ProfilerId profilerId2 = new ProfilerId(PROFILER_NAME_2, RUNS_COUNTER_4);
        Profiler profiler2 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId2))).thenReturn(profiler2);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithStartProfilingsAnnotation"));

        verify(profilingManager).getProfiler(eq(profilerId1));
        verify(profiler1).startStage(STAGE_NAME_1, STAGE_ORDER_1);

        verify(profilingManager).getProfiler(eq(profilerId2));
        verify(profiler2).startStage(STAGE_NAME_2, STAGE_ORDER_2);
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenStartProfilingAnnotationsChildIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithStartProfilingsAnnotationWithDisabledChild"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromBothAnnotationsAndStartProfilers() throws Throwable {
        ProfilerId profilerId1 = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler1 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId1))).thenReturn(profiler1);

        ProfilerId profilerId2 = new ProfilerId(PROFILER_NAME_2, RUNS_COUNTER_4);
        Profiler profiler2 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId2))).thenReturn(profiler2);

        ProfilerId profilerId3 = new ProfilerId(PROFILER_NAME_3, RUNS_COUNTER_3);
        Profiler profiler3 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId3))).thenReturn(profiler3);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithBothAnnotations"));

        verify(profilingManager).getProfiler(eq(profilerId1));
        verify(profiler1).startStage(STAGE_NAME_1, STAGE_ORDER_1);

        verify(profilingManager).getProfiler(eq(profilerId2));
        verify(profiler2).startStage(STAGE_NAME_2, STAGE_ORDER_2);

        verify(profilingManager).getProfiler(eq(profilerId3));
        verify(profiler3).startStage(STAGE_NAME_3, STAGE_ORDER_3);
    }

    /**
     * This test is made just to remove point cut methods from test coverage diagram.
     */
    @Test
    public void dummyTest() {
        aspect.constructor();
        aspect.method();
        aspect.constructorWithMultipleAnnotations();
        aspect.methodWithMultipleAnnotations();
    }

    @After
    public void tearDown() {
        PurySetter.setProfilingManager(null);
        Pury.setEnabled(true);
    }

    private JoinPoint mockJoinPoint(String methodName) throws NoSuchMethodException {
        JoinPoint joinPoint = mock(JoinPoint.class);
        MethodSignature methodSignature = mock(MethodSignature.class);

        when(methodSignature.getMethod()).thenReturn(this.getClass().getDeclaredMethod(methodName));
        when(joinPoint.getSignature()).thenReturn(methodSignature);
        return joinPoint;
    }

    @StartProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
            stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1)
    private void methodWithStartProfilingAnnotation() {
    }

    @StartProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
            stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1, enabled = false)
    private void methodWithDisabledStartProfilingAnnotation() {
    }

    @StartProfilings(value = {
            @StartProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1),
            @StartProfiling(runsCounter = RUNS_COUNTER_4, profilerName = PROFILER_NAME_2,
                    stageName = STAGE_NAME_2, stageOrder = STAGE_ORDER_2)
    })
    private void methodWithStartProfilingsAnnotation() {
    }

    @StartProfilings(value = {
            @StartProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1, enabled = false)
    })
    private void methodWithStartProfilingsAnnotationWithDisabledChild() {
    }

    @StartProfilings(value = {
        @StartProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                stageName = STAGE_NAME_1, stageOrder = STAGE_ORDER_1),
        @StartProfiling(runsCounter = RUNS_COUNTER_4, profilerName = PROFILER_NAME_2,
                stageName = STAGE_NAME_2, stageOrder = STAGE_ORDER_2)
    })
    @StartProfiling(runsCounter = RUNS_COUNTER_3, profilerName = PROFILER_NAME_3,
            stageName = STAGE_NAME_3, stageOrder = STAGE_ORDER_3)
    private void methodWithBothAnnotations() {
    }
}

<|start_filename|>pury-no-op/src/main/java/com/nikitakozlov/pury/Pury.java<|end_filename|>
package com.nikitakozlov.pury;


import com.nikitakozlov.pury.profile.ProfilerId;
import com.nikitakozlov.pury.profile.ProfilingManager;

public final class Pury {

    public static final String LOG_TAG = "Pury";
    public static final String LOGGER_PLUGIN = "LoggerPlugin";

    public static void setLogger(Logger logger) {
    }

    public synchronized static Logger getLogger() {
        return null;
    }

    public static boolean isEnabled() {
        return false;
    }

    public synchronized static void setEnabled(boolean enabled) {
    }

    /**
     * @param profilerName used to identify profiler. Used in results.
     * @param stageName    Name of stage to start. Used in results.
     * @param stageOrder   Stage order must be bigger then order of current most nested active stage.
     *                     First profiling must starts with value 0.
     * @param runsCounter  used to identify profiler. Amount of runs to average.
     *                     Result will be available only after all runs are stopped.
     */
    public static void startProfiling(String profilerName, String stageName, int stageOrder,
                                      int runsCounter) {
    }

    /**
     * @param profilerName used to identify profiler. Used in results.
     * @param stageName    Name of stage to stop. Used in results.
     * @param runsCounter  used to identify profiler. Amount of runs to average.
     *                     Result will be available only after all runs are stopped.
     */
    public static void stopProfiling(String profilerName, String stageName, int runsCounter) {
    }

    /**
     * Adding custom Plugins allows to customize result processing. For example instead of
     * logging result send it somewhere, or save it in a permanent storage. Default
     * {@link Plugin} logs result into {@link Logger}.
     *
     * @param key           that identify Plugin
     * @param plugin Plugin to process result
     */
    public static void addPlugin(String key, Plugin plugin) {
    }

    /**
     * Remove {@link Plugin} by key. To remove default result handler use {@link #LOGGER_PLUGIN}
     *
     * @param key that identify Plugin to remove.
     */
    public static void removePlugin(String key) {
    }

    /**
     * {@link #startProfiling(String, String, int, int)} and {@link #stopProfiling(String, String, int)}
     * creates {@link ProfilerId} on every call. To avoid creating same ProfilerId multiple times,
     * it is possible to call ProfilingManager directly.
     * @return {@link ProfilingManager}
     */
    public static ProfilingManager getProfilingManager() {
        return null;
    }

    static void setProfilingManager(ProfilingManager profilingManager) {
    }
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/PurySetter.java<|end_filename|>
package com.nikitakozlov.pury;

import com.nikitakozlov.pury.profile.ProfilingManager;

public class PurySetter {
    public static void setProfilingManager(ProfilingManager profilingManager) {
        Pury.setProfilingManager(profilingManager);
    }
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/RunFactory.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public class RunFactory {
    public Run startNewRun(String stageName, int stageOrder) {
        return new Run(stageName, stageOrder);
    }
}


<|start_filename|>annotations/src/main/java/com/nikitakozlov/pury/annotations/StopProfiling.java<|end_filename|>
package com.nikitakozlov.pury.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Profiling will stop after method/constructor. Stops first stage that match stage name.
 * All nested stages will be stopped as well.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface StopProfiling {

    /**
     * Profiler Name, used in results.
     */
    String profilerName() default "";

    /**
     * Name of stage to stop. Used in results.
     */
    String stageName() default "";

    /**
     * Amount of runs to average. Result will be available only after all runs are stopped.
     */
    int runsCounter() default 1;

    /**
     * Set to false if you want to skip this annotation.
     */
    boolean enabled() default true;
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/result/ResultVisitor.java<|end_filename|>
package com.nikitakozlov.pury.result;

import com.nikitakozlov.pury.result.model.AverageProfileResult;
import com.nikitakozlov.pury.result.model.RootAverageProfileResult;
import com.nikitakozlov.pury.result.model.RootSingleProfileResult;
import com.nikitakozlov.pury.result.model.SingleProfileResult;

/**
 * To simplify result processing all results make use of Visitor Pattern.
 */
public interface ResultVisitor {
    void visit(AverageProfileResult averageProfileResult);

    void visit(RootAverageProfileResult rootAverageProfileResult);

    void visit(RootSingleProfileResult rootSingleProfileResult);

    void visit(SingleProfileResult singleProfileResult);
}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/Logger.java<|end_filename|>
package com.nikitakozlov.pury;

public interface Logger {
    void result(String tag, String message);

    void warning(String tag, String message);

    void error(String tag, String message);
}


<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/aspects/StopProfilingAspectTest.java<|end_filename|>
package com.nikitakozlov.pury.aspects;

import com.nikitakozlov.pury.Pury;
import com.nikitakozlov.pury.PurySetter;
import com.nikitakozlov.pury.annotations.StopProfiling;
import com.nikitakozlov.pury.annotations.StopProfilings;
import com.nikitakozlov.pury.profile.Profiler;
import com.nikitakozlov.pury.profile.ProfilingManager;
import com.nikitakozlov.pury.profile.ProfilerId;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class StopProfilingAspectTest {

    private static final int RUNS_COUNTER_5 = 5;
    private static final String PROFILER_NAME_1 = "profilerName 1";
    private static final String STAGE_NAME_1 = "stageName 1";


    private static final int RUNS_COUNTER_4 = 4;
    private static final String PROFILER_NAME_2 = "profilerName 2";
    private static final String STAGE_NAME_2 = "stageName 2";


    private static final int RUNS_COUNTER_3 = 3;
    private static final String PROFILER_NAME_3 = "profilerName 3";
    private static final String STAGE_NAME_3 = "stageName 3";

    private ProfilingManager profilingManager;
    private StopProfilingAspect aspect;

    @Before
    public void setUp() {
        profilingManager = mock(ProfilingManager.class);
        PurySetter.setProfilingManager(profilingManager);
        aspect = new StopProfilingAspect();
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromStartProfilingAnnotationAndStartAsyncProfiler() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId))).thenReturn(profiler);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithStopProfilingAnnotation"));

        verify(profilingManager).getProfiler(eq(profilerId));
        verify(profiler).stopStage(STAGE_NAME_1);
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenStopProfilingAnnotationIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithDisabledStopProfilingAnnotation"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenPuryIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        Pury.setEnabled(false);
        aspect.weaveJoinPoint(mockJoinPoint("methodWithStopProfilingAnnotation"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromStopProfilingsAnnotationAndStartProfilers() throws Throwable {
        ProfilerId profilerId1 = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler1 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId1))).thenReturn(profiler1);

        ProfilerId profilerId2 = new ProfilerId(PROFILER_NAME_2, RUNS_COUNTER_4);
        Profiler profiler2 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId2))).thenReturn(profiler2);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithStopProfilingsAnnotation"));

        verify(profilingManager).getProfiler(eq(profilerId1));
        verify(profiler1).stopStage(STAGE_NAME_1);

        verify(profilingManager).getProfiler(eq(profilerId2));
        verify(profiler2).stopStage(STAGE_NAME_2);
    }

    @Test
    public void weaveJoinPoint_DoesNothing_WhenStopProfilingAnnotationsChildIsDisabled() throws Throwable {
        ProfilerId profilerId = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithStopProfilingsAnnotationWithDisabledChild"));

        verify(profilingManager, never()).getProfiler(eq(profilerId));
    }

    @Test
    public void weaveJoinPoint_TakesParametersFromBothAnnotationsAndStopProfilers() throws Throwable {
        ProfilerId profilerId1 = new ProfilerId(PROFILER_NAME_1, RUNS_COUNTER_5);
        Profiler profiler1 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId1))).thenReturn(profiler1);

        ProfilerId profilerId2 = new ProfilerId(PROFILER_NAME_2, RUNS_COUNTER_4);
        Profiler profiler2 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId2))).thenReturn(profiler2);

        ProfilerId profilerId3 = new ProfilerId(PROFILER_NAME_3, RUNS_COUNTER_3);
        Profiler profiler3 = mock(Profiler.class);
        when(profilingManager.getProfiler(eq(profilerId3))).thenReturn(profiler3);

        aspect.weaveJoinPoint(mockJoinPoint("methodWithBothAnnotations"));

        verify(profilingManager).getProfiler(eq(profilerId1));
        verify(profiler1).stopStage(STAGE_NAME_1);

        verify(profilingManager).getProfiler(eq(profilerId2));
        verify(profiler2).stopStage(STAGE_NAME_2);

        verify(profilingManager).getProfiler(eq(profilerId3));
        verify(profiler3).stopStage(STAGE_NAME_3);
    }

    /**
     * This test is made just to remove point cut methods from test coverage diagram.
     */
    @Test
    public void dummyTest() {
        aspect.constructor();
        aspect.method();
        aspect.constructorWithMultipleAnnotations();
        aspect.methodWithMultipleAnnotations();
    }

    @After
    public void tearDown() {
        PurySetter.setProfilingManager(null);
        Pury.setEnabled(true);
    }

    private JoinPoint mockJoinPoint(String methodName) throws NoSuchMethodException {
        JoinPoint joinPoint = mock(ProceedingJoinPoint.class);
        MethodSignature methodSignature = mock(MethodSignature.class);

        when(methodSignature.getMethod()).thenReturn(this.getClass().getDeclaredMethod(methodName));
        when(joinPoint.getSignature()).thenReturn(methodSignature);
        return joinPoint;
    }

    @StopProfiling(runsCounter = RUNS_COUNTER_5, stageName = STAGE_NAME_1,
            profilerName = PROFILER_NAME_1)
    private void methodWithStopProfilingAnnotation() {
    }

    @StopProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
            stageName = STAGE_NAME_1, enabled = false)
    private void methodWithDisabledStopProfilingAnnotation() {
    }

    @StopProfilings(value = {
            @StopProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1),
            @StopProfiling(runsCounter = RUNS_COUNTER_4, profilerName = PROFILER_NAME_2,
                    stageName = STAGE_NAME_2)
    })
    private void methodWithStopProfilingsAnnotation() {
    }

    @StopProfilings(value = {
            @StopProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1, enabled = false)
    })
    private void methodWithStopProfilingsAnnotationWithDisabledChild() {
    }

    @StopProfilings(value = {
            @StopProfiling(runsCounter = RUNS_COUNTER_5, profilerName = PROFILER_NAME_1,
                    stageName = STAGE_NAME_1),
            @StopProfiling(runsCounter = RUNS_COUNTER_4, profilerName = PROFILER_NAME_2,
                    stageName = STAGE_NAME_2)
    })
    @StopProfiling(runsCounter = RUNS_COUNTER_3, profilerName = PROFILER_NAME_3,
            stageName = STAGE_NAME_3)
    private void methodWithBothAnnotations() {
    }

}

<|start_filename|>annotations/src/main/java/com/nikitakozlov/pury/annotations/MethodProfiling.java<|end_filename|>
package com.nikitakozlov.pury.annotations;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Combination of {@link StartProfiling} and {@link StopProfiling}. If stage name is empty, then stage name from
 * method's name and class will be generated.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface MethodProfiling {
    /**
     * Profiler Name, used in results.
     */
    String profilerName() default "";

    /**
     * Name of stage to start. Used in results. If stage name is empty, then stage name from
     * method's name and class will be generated.
     */
    String stageName() default "";

    /**
     * Stage order must be bigger then order of current most nested active stage.
     * First profiling must starts with value 0.
     */
    int stageOrder() default 0;

    /**
     * Amount of runs to average. Result will be available only after all runs are stopped.
     */
    int runsCounter() default 1;

    /**
     * Set to false if you want to skip this annotation.
     */
    boolean enabled() default true;
}

<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/ProfilerId.java<|end_filename|>
package com.nikitakozlov.pury.profile;

/**
 * Used to identify Profiler
 */
public class ProfilerId {
    private final String mProfilerName;
    private final int mRunsCounter;

    /**
     * @param profilerName Profiler Name.
     * @param runsCounter Amount of runs to complete
     */
    public ProfilerId(String profilerName, int runsCounter) {
        this.mProfilerName = profilerName;
        this.mRunsCounter = runsCounter;
    }

    /**
     * @return Profiler Name
     */
    public String getProfilerName() {
        return mProfilerName;
    }

    /**
     * @return RunCounter that represents amount of runs to complete
     */
    public int getRunsCounter() {
        return mRunsCounter;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        ProfilerId that = (ProfilerId) o;

        if (mRunsCounter != that.mRunsCounter) return false;
        return mProfilerName != null ? mProfilerName.equals(that.mProfilerName) : that.mProfilerName == null;

    }

    @Override
    public int hashCode() {
        int result = mProfilerName != null ? mProfilerName.hashCode() : 0;
        result = 31 * result + mRunsCounter;
        return result;
    }
}


<|start_filename|>pury-no-op/src/main/java/com/nikitakozlov/pury/profile/ProfilerId.java<|end_filename|>
package com.nikitakozlov.pury.profile;

/**
 * Used to identify Profiler
 */
public class ProfilerId {
    /**
     * @param profilerName Profiler Name.
     * @param runsCounter Amount of runs to complete
     */
    public ProfilerId(String profilerName, int runsCounter) {
    }

    /**
     * @return Profiler Name
     */
    public String getProfilerName() {
        return null;
    }

    /**
     * @return RunCounter that represents amount of runs to complete
     */
    public int getRunsCounter() {
        return 0;
    }

}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/ProfileResultProcessor.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import com.nikitakozlov.pury.result.model.AverageProfileResult;
import com.nikitakozlov.pury.result.model.AverageTime;
import com.nikitakozlov.pury.result.model.ProfileResult;
import com.nikitakozlov.pury.result.model.RootAverageProfileResult;
import com.nikitakozlov.pury.result.model.RootSingleProfileResult;
import com.nikitakozlov.pury.result.model.SingleProfileResult;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ProfileResultProcessor {
    public ProfileResult process(List<Run> runs) {
        if (runs.isEmpty()) {
            return null;
        }
        List<RootSingleProfileResult> results = new ArrayList<>();
        for (Run run : runs) {
            results.add(processSingleRun(run));
        }

        if (results.size() == 1) {
            return results.get(0);
        }
        return takeAverage(results);
    }

    private RootSingleProfileResult processSingleRun(Run run) {
        Stage rootStage = run.getRootStage();
        return new RootSingleProfileResult(rootStage.getName(), rootStage.getExecTime(),
                processStageList(rootStage.getStages(), rootStage.getStartTime(), 1));
    }

    private List<SingleProfileResult> processStageList(List<Stage> stages, long rootStartTime, int depth) {
        if (stages.isEmpty()) {
            return Collections.emptyList();
        }
        List<SingleProfileResult> results = new ArrayList<>(stages.size());
        for (Stage stage : stages) {
            results.add(processSingleStage(stage, rootStartTime, depth));
        }
        return results;
    }

    private SingleProfileResult processSingleStage(Stage stage, long rootStartTime, int depth) {
        long relativeStartTime = stage.getStartTime() - rootStartTime;

        return new SingleProfileResult(stage.getName(), relativeStartTime,
                stage.getExecTime(), processStageList(stage.getStages(),
                rootStartTime, depth + 1), depth);
    }

    private ProfileResult takeAverage(List<RootSingleProfileResult> singleProfileResults) {
        return new RootAverageProfileResult(singleProfileResults.get(0).getStageName(),
                getAverageExecTime(singleProfileResults),
                getAverageProfileResults(transpose(singleProfileResults), 1));
    }

    private List<List<SingleProfileResult>> transpose(List<? extends ProfileResult> inputResults) {
        List<List<SingleProfileResult>> outputResult = new ArrayList<>();
        int nestedResultsCounter = inputResults.get(0).getNestedResults().size();
        for (int i = 0; i < nestedResultsCounter; i++) {
            List<SingleProfileResult> resultRow = new ArrayList<>(inputResults.size());
            for (ProfileResult inputResult : inputResults) {
                List<? extends ProfileResult> nestedResults = inputResult.getNestedResults();
                if (nestedResults.size() > i) {
                    resultRow.add((SingleProfileResult) nestedResults.get(i));
                }
            }
            outputResult.add(resultRow);
        }
        return outputResult;
    }

    /**
     *
     * @param inputResults transposed.
     * @return average
     */
    private List<AverageProfileResult> getAverageProfileResults(List<List<SingleProfileResult>> inputResults, int depth) {
        if (inputResults.isEmpty()) {
            return Collections.emptyList();
        }

        List<AverageProfileResult> results = new ArrayList<>();
        for (List<SingleProfileResult> profileResults : inputResults) {
            results.add(new AverageProfileResult(profileResults.get(0).getStageName(),
                    getAverageStartTime(profileResults),
                    getAverageExecTime(profileResults),
                    getAverageProfileResults(transpose(profileResults), depth + 1), depth)
            );
        }

        return results;
    }

    private static AverageTime getAverageStartTime(List<? extends ProfileResult> singleProfileResults) {
        List<Long> times = new ArrayList<>(singleProfileResults.size());
        for (ProfileResult profileResult : singleProfileResults) {
            times.add(((SingleProfileResult) profileResult).getStartTime());
        }
        return getAverageTime(times);
    }

    private static AverageTime getAverageExecTime(List<? extends ProfileResult> singleProfileResults) {
        List<Long> times = new ArrayList<>(singleProfileResults.size());
        for (ProfileResult profileResult : singleProfileResults) {
            times.add(((SingleProfileResult) profileResult).getExecTime());
        }
        return getAverageTime(times);
    }

    private static AverageTime getAverageTime(List<Long> times) {
        if (times.isEmpty()) {
            return new AverageTime(0, 0, 0, 0);
        }
        int runCounter = times.size();
        long min = Collections.min(times);
        long max = Collections.max(times);
        return new AverageTime(getAverage(times), min, max, runCounter);
    }

    private static double getAverage(List<Long> longs) {
        double average = 0;
        if (!longs.isEmpty()) {
            int size = longs.size();
            for (Long aLong : longs) {
                average += aLong.doubleValue() / size;
            }
            return average;
        }
        return average;
    }
}


<|start_filename|>annotations/src/main/java/com/nikitakozlov/pury/annotations/StartProfiling.java<|end_filename|>
package com.nikitakozlov.pury.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Profiling will start before method/constructor.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface StartProfiling {
    /**
     * Profiler Name, used in results.
     */
    String profilerName() default "";

    /**
     * Name of stage to start. Used in results.
     */
    String stageName() default "";

    /**
     * Stage order must be bigger then order of current most nested active stage.
     * First profiling must starts with value 0.
     */
    int stageOrder() default 0;

    /**
     * Amount of runs to average. Result will be available only after all runs are stopped.
     */
    int runsCounter() default 1;

    /**
     * Set to false if you want to skip this annotation.
     */
    boolean enabled() default true;
}




<|start_filename|>pury/src/test/java/com/nikitakozlov/pury/profile/RunFactoryTest.java<|end_filename|>
package com.nikitakozlov.pury.profile;

import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

public class RunFactoryTest {

    private static final String STAGE_NAME_0 = "stage_name_0";
    private static final int STAGE_ORDER_0 = 0;

    @Test
    public void startRun_ReturnsRunWithStartedStage() {

        Run run = new RunFactory().startNewRun(STAGE_NAME_0, STAGE_ORDER_0);
        Stage rootStage = run.getRootStage();
        assertFalse(run.isStopped());
        assertTrue(rootStage.isStarted());
        assertEquals(STAGE_NAME_0, rootStage.getName());
        assertEquals(STAGE_ORDER_0, rootStage.getOrder());
    }

}

<|start_filename|>pury-no-op/src/main/java/com/nikitakozlov/pury/profile/Profiler.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public class Profiler {

    /**
     * @param stageName  Name of a stage to start. Used in results.
     * @param stageOrder Stage order must be bigger then order of current most nested active stage.
     *                   First profiling must starts with value 0.
     */
    public synchronized void startStage(String stageName, int stageOrder) {
    }
    /**
     * @param stageName Name of stage to stop. Used in results.
     */
    public synchronized void stopStage(String stageName) {
    }

}


<|start_filename|>pury/src/main/java/com/nikitakozlov/pury/profile/StageError.java<|end_filename|>
package com.nikitakozlov.pury.profile;

public class StageError {

    public static StageError createError(String intendedStageName, Stage parentStage, Type type) {
        return createError(intendedStageName, 0, parentStage, type);
    }

    public static StageError createError(String intendedStageName, int intendedStageOrder,
        Stage parentStage, Type type) {
        return new StageError(intendedStageName, intendedStageOrder, parentStage.getName(),
                parentStage.getOrder(), type);
    }

    private final String intendedStageName;
    private final int intendedStageOrder;
    private final String parentStageName;
    private final int parentStageOrder;
    private final Type type;

    private StageError(String intendedStageName, int intendedStageOrder, String parentStageName,
                      int parentStageOrder, Type type) {
        this.intendedStageName = intendedStageName;
        this.intendedStageOrder = intendedStageOrder;
        this.parentStageName = parentStageName;
        this.parentStageOrder = parentStageOrder;
        this.type = type;
    }

    public Type getType() {
        return type;
    }

    public String getIntendedStageName() {
        return intendedStageName;
    }

    public int getIntendedStageOrder() {
        return intendedStageOrder;
    }

    public String getParentStageName() {
        return parentStageName;
    }

    public int getParentStageOrder() {
        return parentStageOrder;
    }

    public boolean isInternal() {
        return type.isInternal();
    }

    public enum Type {

        START_TO_SMALL_ORDER,
        START_PARENT_STAGE_IS_STOPPED(true),
        START_PARENT_STAGE_NOT_STARTED(true),
        STOP_NOT_STARTED_STAGE,
        STOPPED_ALREADY;

        private final boolean internal;

        Type() {
            internal = false;
        }

        Type(boolean internal) {
            this.internal = internal;
        }

        public boolean isInternal() {
            return internal;
        }
    }
}
