BuildTimeProfiler.java

package com.soebes.maven.extensions;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import com.soebes.maven.extensions.artifact.DeployTimer;
import com.soebes.maven.extensions.artifact.DownloadTimer;
import com.soebes.maven.extensions.artifact.InstallTimer;
import com.soebes.maven.extensions.metadata.MetadataDeploymentTimer;
import com.soebes.maven.extensions.metadata.MetadataDownloadTimer;
import com.soebes.maven.extensions.metadata.MetadataInstallTimer;
import org.apache.maven.execution.ExecutionEvent;
import org.apache.maven.execution.MavenExecutionRequest;
import org.apache.maven.execution.MavenExecutionResult;
import org.apache.maven.project.DependencyResolutionRequest;
import org.apache.maven.project.DependencyResolutionResult;
import org.apache.maven.project.MavenProject;
import org.eclipse.aether.RepositoryEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Named;
import javax.inject.Singleton;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * @author Karl Heinz Marbaise <a href="mailto:kama@soebes.de">kama@soebes.de</a>
 */
@Named
@Singleton
public class BuildTimeProfiler
    extends LifeCycleOrdering
{
    private static final String SEPARATION_LINE = "------------------------------------------------------------------------";

    private static final Logger LOGGER = LoggerFactory.getLogger(BuildTimeProfiler.class);

    private final List<String> lifeCyclePhases;

    private final DiscoveryTimer discoveryTimer;

    private final GoalTimer goalTimer;

    private final MojoTimer mojoTimer;

    private final ProjectTimer projectTimer;

    private final SessionTimer sessionTimer;

    private final InstallTimer installTimer;

    private final DeployTimer deployTimer;

    private final DownloadTimer downloadTimer;

    private final MetadataDownloadTimer metadataDownloadTimer;

    private final MetadataDeploymentTimer metadataDeploymentTimer;

    private final MetadataInstallTimer metadataInstallTimer;

    private final ForkTimer forkTimer;

    private final ProjectTimer forkProject;

    private boolean activated;

    public BuildTimeProfiler()
    {
        LOGGER.debug( "LifeCycleProfiler ctor called." );
        this.lifeCyclePhases = Collections.synchronizedList(new LinkedList<>() );
        this.discoveryTimer = new DiscoveryTimer();
        this.goalTimer = new GoalTimer();
        this.mojoTimer = new MojoTimer();
        this.projectTimer = new ProjectTimer();
        this.sessionTimer = new SessionTimer();
        this.installTimer = new InstallTimer();
        this.deployTimer = new DeployTimer();
        this.downloadTimer = new DownloadTimer();

        this.metadataDownloadTimer = new MetadataDownloadTimer();
        this.metadataDeploymentTimer = new MetadataDeploymentTimer();
        this.metadataInstallTimer = new MetadataInstallTimer();
        this.forkTimer = new ForkTimer();
        this.forkProject = new ProjectTimer();

        String disabled = System.getProperty("maven-build-time-profiler.disabled", "unknown");
        LOGGER.debug("MBTP: maven-build-time-profiler.disabled {}", disabled);
        this.activated = !disabled.equals("true");
    }

    @Override
    public void init( Context context )
        throws Exception
    {
        super.init( context );

        LOGGER.info( "Maven Build Time Profiler started. (Version {})", BuildTimeProfilerVersion.getVersion() );
        if ( !activated ) {
            LOGGER.info("Maven Build Time Profiler deactivated.");
            return;
        }

        // Is this always in the context? Based on Maven Core yes.
        String workingDirectory = (String) context.getData().get( "workingDirectory" );
        LOGGER.debug( "MBTP: workingDirectory: {}", workingDirectory );

        String multiModuleProjectDirectory = (String) context.getData().get( "multiModuleProjectDirectory" );
        LOGGER.debug( "MBTP: multiModuleProjectDirectory: {}", multiModuleProjectDirectory );
    }

    @Override
    public void onEvent( Object event )
        throws Exception
    {
        try
        {
            if ( !activated ) {
                return;
            }

            if ( event instanceof ExecutionEvent)
            {
                executionEventHandler( (ExecutionEvent) event );
            }
            else if ( event instanceof org.eclipse.aether.RepositoryEvent )
            {
                repositoryEventHandler( (RepositoryEvent) event );
            }
            else if ( event instanceof MavenExecutionRequest)
            {
                executionRequestEventHandler( (MavenExecutionRequest) event );
            }
            else if ( event instanceof MavenExecutionResult)
            {
                executionResultEventHandler( (MavenExecutionResult) event );
            }
            else if ( event instanceof DependencyResolutionRequest)
            {
                dependencyResolutionRequest( (DependencyResolutionRequest) event );
            }
            else if ( event instanceof DependencyResolutionResult)
            {
                dependencyResolutionResult( (DependencyResolutionResult) event );
            }
            // The following event type is available since Maven 3.3.1+
            // else if ( event instanceof DefaultSettingsBuildingRequest) {
            // DefaultSettingsBuildingRequest r = null;
            // r.getGlobalSettingsFile();
            // r.getGlobalSettingsSource();
            // r.getSystemProperties();
            // r.getUserSettingsFile();
            // r.getUserSettingsSource();
            //
            // r.setGlobalSettingsFile( globalSettingsFile );
            // r.setGlobalSettingsSource( globalSettingsSource );
            // r.setSystemProperties( systemProperties );
            // r.setUserProperties( userProperties );
            // r.setUserSettingsFile( userSettingsFile );
            // r.setUserSettingsSource( userSettingsSource );
            // }
            // The following event type is available since Maven 3.3.1+
            // else if (event instanceof DefaultSettingsBuildingRequest) {
            //
            // DefaultSettingsBuildingRequest r = null;
            // r.getGlobalSettingsSource().getLocation()
            // }
            // The following event type is available since Maven 3.3.1+
            // else if (event instanceof DefaultToolchainsBuildingRequest) {
            // DefaultToolchainsBuildingRequest r = null;
            // r.getGlobalToolchainsSource().
            // }
            // The following event type is available since Maven 3.3.1+
            // else if (event instanceof DefaultToolchainsBuildingResult) {
            // DefaultToolchainsBuildingResult r = null;
            // r.getEffectiveToolchains();
            // r.getProblems();
            // }
            else
            {
                // TODO: What kind of event we haven't considered?
                LOGGER.debug( "MBTP: Event {}", event.getClass().getCanonicalName() );
            }
        }
        catch ( Exception e )
        {
            LOGGER.error( "MBTP: Exception", e );
        }
    }

    @Override
    public void close()
    {
        if ( !activated ) {
            return;
        }
        LOGGER.debug( "MBTP: done." );
    }

    private void dependencyResolutionResult( DependencyResolutionResult event )
    {
        LOGGER.debug( "MBTP: dependencyResolutionResult() {}", event.getResolvedDependencies().size() );
    }

    private void dependencyResolutionRequest( DependencyResolutionRequest event )
    {
        LOGGER.debug( "MBTP: dependencyResolutionRequest()" );
    }

    private void repositoryEventHandler( org.eclipse.aether.RepositoryEvent repositoryEvent )
    {
        RepositoryEvent.EventType type = repositoryEvent.getType();
        switch ( type )
        {
            case ARTIFACT_DOWNLOADING:
                LOGGER.debug( "MBTP: repositoryEventHandler {}", type );
                downloadTimer.start( repositoryEvent );
                break;
            case ARTIFACT_DOWNLOADED:
                LOGGER.debug( "MBTP: repositoryEventHandler {}", type );
                downloadTimer.stop( repositoryEvent );
                break;

            case ARTIFACT_DEPLOYING:
                deployTimer.start( repositoryEvent );
                break;
            case ARTIFACT_DEPLOYED:
                deployTimer.stop( repositoryEvent );
                break;

            case ARTIFACT_INSTALLING:
                installTimer.start( repositoryEvent );
                break;
            case ARTIFACT_INSTALLED:
                installTimer.stop( repositoryEvent );
                break;

            case METADATA_DEPLOYING:
                metadataDeploymentTimer.start( repositoryEvent );
                break;
            case METADATA_DEPLOYED:
                metadataDeploymentTimer.stop( repositoryEvent );
                break;

            case METADATA_DOWNLOADING:
                LOGGER.debug( "MBTP: repositoryEventHandler {}", type );
                metadataDownloadTimer.start( repositoryEvent );
                break;
            case METADATA_DOWNLOADED:
                LOGGER.debug( "MBTP: repositoryEventHandler {}", type );
                metadataDownloadTimer.stop( repositoryEvent );
                break;

            case METADATA_INSTALLING:
                LOGGER.debug( "MBTP: repositoryEventHandler {}", type );
                metadataInstallTimer.start( repositoryEvent );
                break;
            case METADATA_INSTALLED:
                LOGGER.debug( "MBTP: repositoryEventHandler {}", type );
                metadataInstallTimer.stop( repositoryEvent );
                break;

            case ARTIFACT_RESOLVING:
            case ARTIFACT_RESOLVED:
            case ARTIFACT_DESCRIPTOR_INVALID:
            case ARTIFACT_DESCRIPTOR_MISSING:
            case METADATA_RESOLVED:
            case METADATA_RESOLVING:
            case METADATA_INVALID:
                // Those events are not recorded.
                break;

            default:
                LOGGER.error( "MBTP: repositoryEventHandler {}", type );
                break;
        }
    }

    private void executionEventHandler( ExecutionEvent executionEvent )
    {
        if ( !activated ) {
            return;
        }
        LOGGER.debug( "executionEventHandler: {}", executionEvent.getType() );
        ExecutionEvent.Type type = executionEvent.getType();
        switch ( type )
        {
            case ProjectDiscoveryStarted:
                // Start reading the pom files..
                discoveryTimer.discoveryStart();
                break;
            case SessionStarted:
                // Reading of pom files done and structure now there.
                discoveryTimer.discoveryStop();
                sessionTimer.sessionStart();
                break;
            case SessionEnded:
                // Everything is done.
                LOGGER.info( "MBTP: Session Ended");
                sessionTimer.sessionStop();
                break;

            case ForkStarted:
                forkTimer.start();
                break;
            case ForkFailed:
            case ForkSucceeded:
                forkTimer.stop();
                break;

            case ForkedProjectStarted:
                forkProject.projectStart( executionEvent );
                break;
            case ForkedProjectFailed:
            case ForkedProjectSucceeded:
                forkProject.projectStop( executionEvent );
                break;

            case MojoStarted:
                String phaseStart = executionEvent.getMojoExecution().getLifecyclePhase();
                // Key: phase, project, mojo
                if ( phaseStart == null )
                {
                    goalTimer.mojoStart( executionEvent );
                }
                else
                {
                    collectAllLifeCyclePhases( phaseStart );
                    mojoTimer.mojoStart( executionEvent );
                }
                break;

            case MojoFailed:
            case MojoSucceeded:
            case MojoSkipped:
                String phaseStop = executionEvent.getMojoExecution().getLifecyclePhase();
                if ( phaseStop == null )
                {
                    goalTimer.mojoStop( executionEvent );
                }
                else
                {
                    mojoTimer.mojoStop( executionEvent );
                }
                break;

            case ProjectStarted:
                projectTimer.projectStart( executionEvent );
                break;

            case ProjectFailed:
            case ProjectSucceeded:
            case ProjectSkipped:
                projectTimer.projectStop( executionEvent );
                break;

            default:
                LOGGER.error( "MBTP: executionEventHandler: {}", type );
                break;
        }

    }

    private void executionRequestEventHandler( MavenExecutionRequest event )
    {
        LOGGER.debug( "MBTP: executionRequestEventHandler: {}", event.getExecutionListener() );
    }

    private void executionResultEventHandler( MavenExecutionResult event )
    {
        LOGGER.debug( "MBTP: executionResultEventHandler: {}", event.getProject() );
        LOGGER.info( "--             Maven Build Time Profiler Summary                      --" );
        LOGGER.info(SEPARATION_LINE);

        discoveryTimer.report();

        if ( mojoTimer.hasEvents() )
        {
            LOGGER.info( "Project Build Time (reactor order):" );
            LOGGER.info( "" );
            for ( MavenProject mavenProject : event.getTopologicallySortedProjects() )
            {
                LOGGER.info( "{}:", mavenProject.getName() );

                for ( String phase : lifeCyclePhases )
                {
                    ProjectKey projectKey = mavenProjectToProjectKey( mavenProject );

                    if ( !mojoTimer.hasTimeForProjectAndPhase( projectKey, phase ) )
                    {
                        continue;
                    }

                    long timeForPhaseAndProjectInMillis =
                        mojoTimer.getTimeForProjectAndPhaseInMillis( projectKey, phase );
                    String formattedTime = String.format("%8d", timeForPhaseAndProjectInMillis);
                    LOGGER.info( "    {} ms : {}", formattedTime, phase );

                }

            }
            LOGGER.info(SEPARATION_LINE);
            LOGGER.info( "Lifecycle Phase summary:" );
            LOGGER.info( "" );
            for ( String phase : lifeCyclePhases )
            {
                long timeForPhaseInMillis = mojoTimer.getTimeForPhaseInMillis( phase );
                LOGGER.info( "{} ms : {}", String.format( "%8d", timeForPhaseInMillis ), phase );
            }

            // List all plugins per phase
            LOGGER.info(SEPARATION_LINE);
            LOGGER.info( "Plugins in lifecycle Phases:" );
            LOGGER.info( "" );
            for ( String phase : lifeCyclePhases )
            {
                LOGGER.info( "{}:", phase );
                Map<ProjectMojo, SystemTime> plugisInPhase = mojoTimer.getPluginsInPhase( phase );
                for ( Entry<ProjectMojo, SystemTime> pluginInPhase : plugisInPhase.entrySet() )
                {
                    LOGGER.info( "{} ms: {}", String.format( "%8d", pluginInPhase.getValue().getElapsedTime() ),
                                 pluginInPhase.getKey().getMojo().getFullId() );
                }

            }

            LOGGER.info( "" );
            long timeForPlugins = mojoTimer.getTimeForPlugins();
            String formattedTime = String.format("%8d", timeForPlugins);
            LOGGER.info("{} ms plugins execution summary.", formattedTime);
            LOGGER.info(SEPARATION_LINE);
        }

        if ( goalTimer.hasEvents() )
        {
            LOGGER.info( "Plugins directly called via goals:" );
            LOGGER.info( "" );
            goalTimer.report();
            LOGGER.info(SEPARATION_LINE);
        }

        installTimer.report();
        downloadTimer.report();
        deployTimer.report();
        metadataInstallTimer.report();
        metadataDownloadTimer.report();
        metadataDeploymentTimer.report();

        forkTimer.report();
        forkProject.report();
    }

    private ProjectKey mavenProjectToProjectKey( MavenProject project )
    {
        return new ProjectKey( project.getGroupId(), project.getArtifactId(), project.getVersion() );
    }

    private void collectAllLifeCyclePhases(String phase )
    {
        // Phase can be null if you call maven via:
        // mvn site:stage (no life cycle started.)
        if ( phase == null )
        {
            return;
        }
        LOGGER.debug( "collectAllLifeCyclePhases({})", phase );
        if ( !lifeCyclePhases.contains( phase ) )
        {
            lifeCyclePhases.add( phase );
        }
    }

}