All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.takari.maven.builder.smart.ReactorBuildStats Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2014-2024 Takari, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache Software License v2.0
 * which accompanies this distribution, and is available at
 * https://www.apache.org/licenses/LICENSE-2.0
 */
package io.takari.maven.builder.smart;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.project.MavenProject;

class ReactorBuildStats {

    private long startTime;

    private long stopTime;

    /**
     * Time, in nanoseconds, a worker thread was executing the project build lifecycle. In addition to
     * Maven plugin goals execution includes any "overhead" time Maven spends resolving project
     * dependencies, calculating build time and perform any post-execution cleanup and maintenance.
     */
    private final Map serviceTimes;

    /**
     * Time, in nanoseconds, when the project was a bottleneck of entire build, i.e. when not all
     * available CPU cores were utilized, presumably because the project build time and dependency
     * structure prevented higher degree of parallelism.
     */
    private final Map bottleneckTimes;

    private ReactorBuildStats(Map serviceTimes, Map bottleneckTimes) {
        this.serviceTimes = Collections.unmodifiableMap(serviceTimes);
        this.bottleneckTimes = Collections.unmodifiableMap(bottleneckTimes);
    }

    private static String projectGAV(MavenProject project) {
        return project.getGroupId() + ":" + project.getArtifactId() + ":" + project.getVersion();
    }

    public static ReactorBuildStats create(Collection projects) {
        Map serviceTimes = new HashMap<>();
        Map bottleneckTimes = new HashMap<>();
        projects.stream().map(ReactorBuildStats::projectGAV).forEach(key -> {
            serviceTimes.put(key, new AtomicLong());
            bottleneckTimes.put(key, new AtomicLong());
        });
        return new ReactorBuildStats(serviceTimes, bottleneckTimes);
    }

    public void recordStart() {
        this.startTime = System.nanoTime();
    }

    public void recordStop() {
        this.stopTime = System.nanoTime();
    }

    public void recordServiceTime(MavenProject project, long durationNanos) {
        AtomicLong serviceTime = serviceTimes.get(projectGAV(project));
        if (serviceTime == null) {
            throw new IllegalStateException(
                    "Unknown project " + projectGAV(project) + ", found " + serviceTimes.keySet());
        }
        serviceTime.addAndGet(durationNanos);
    }

    public void recordBottlenecks(Set projects, int degreeOfConcurrency, long durationNanos) {
        // only projects that result in single-threaded builds
        if (projects.size() == 1) {
            projects.forEach(p -> bottleneckTimes.get(projectGAV(p)).addAndGet(durationNanos));
        }
    }

    //
    // Reporting
    //

    public long totalServiceTime(TimeUnit unit) {
        long nanos =
                serviceTimes.values().stream().mapToLong(AtomicLong::longValue).sum();
        return unit.convert(nanos, TimeUnit.NANOSECONDS);
    }

    public long walltimeTime(TimeUnit unit) {
        return unit.convert(stopTime - startTime, TimeUnit.NANOSECONDS);
    }

    public String renderCriticalPath(DependencyGraph graph) {
        return renderCriticalPath(graph, ReactorBuildStats::projectGAV);
    }

    public  String renderCriticalPath(DependencyGraph graph, Function toKey) {
        StringBuilder result = new StringBuilder();

        // render critical path

        long criticalPathServiceTime = 0;
        result.append("Build critical path service times (and bottleneck** times):");
        for (K project : calculateCriticalPath(graph, toKey)) {
            result.append('\n');
            String key = toKey.apply(project);
            criticalPathServiceTime += serviceTimes.get(key).get();
            appendProjectTimes(result, key);
        }
        result.append(
                String.format("\nBuild critical path total service time %s", formatDuration(criticalPathServiceTime)));

        // render bottleneck projects

        List bottleneckProjects = getBottleneckProjects();
        if (!bottleneckProjects.isEmpty()) {
            long bottleneckTotalTime = 0;
            result.append("\nBuild bottleneck projects service times (and bottleneck** times):");
            for (String bottleneck : bottleneckProjects) {
                result.append('\n');
                bottleneckTotalTime += bottleneckTimes.get(bottleneck).get();
                appendProjectTimes(result, bottleneck);
            }
            result.append(String.format("\nBuild bottlenecks total time %s", formatDuration(bottleneckTotalTime)));
        }

        result.append("\n** Bottlenecks are projects that limit build concurrency");
        result.append("\n   removing bottlenecks improves overall build time");
        return result.toString();
    }

    private void appendProjectTimes(StringBuilder result, String project) {
        final long serviceTime = serviceTimes.get(project).get();
        final long bottleneckTime = bottleneckTimes.get(project).get();
        result.append(String.format("   %-60s %s", project, formatDuration(serviceTime)));
        if (bottleneckTime > 0) {
            result.append(String.format(" (%s)", formatDuration(bottleneckTime)));
        }
    }

    public List getBottleneckProjects() {
        Comparator comparator = (a, b) -> {
            long ta = bottleneckTimes.get(a).longValue();
            long tb = bottleneckTimes.get(b).longValue();
            if (tb > ta) {
                return 1;
            } else if (tb < ta) {
                return -1;
            }
            return 0;
        };
        return bottleneckTimes.keySet().stream() //
                .sorted(comparator) //
                .filter(project -> bottleneckTimes.get(project).get() > 0) //
                .collect(Collectors.toList());
    }

    private String formatDuration(long nanos) {
        long secs = TimeUnit.NANOSECONDS.toSeconds(nanos);
        return String.format("%5d s", secs);
    }

    private  List calculateCriticalPath(DependencyGraph graph, Function toKey) {
        Comparator comparator = ProjectComparator.create0(graph, serviceTimes, toKey);
        Stream rootProjects = graph.getProjects().filter(graph::isRoot);
        List criticalPath = new ArrayList<>();
        K project = getCriticalProject(rootProjects, comparator);
        do {
            criticalPath.add(project);
        } while ((project = getCriticalProject(graph.getDownstreamProjects(project), comparator)) != null);
        return criticalPath;
    }

    private  K getCriticalProject(Stream projects, Comparator comparator) {
        return projects.min(comparator).orElse(null);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy