nebula.plugin.metrics.collector.GradleBuildMetricsCollector Maven / Gradle / Ivy
/*
* Copyright 2015-2018 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package nebula.plugin.metrics.collector;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import nebula.plugin.metrics.MetricsLoggerFactory;
import nebula.plugin.metrics.dispatcher.MetricsDispatcher;
import nebula.plugin.metrics.model.GradleToolContainer;
import nebula.plugin.metrics.model.Info;
import nebula.plugin.metrics.model.Result;
import nebula.plugin.metrics.model.BuildMetrics;
import nebula.plugin.metrics.model.CompositeOperation;
import nebula.plugin.metrics.model.ContinuousOperation;
import nebula.plugin.metrics.model.ProjectMetrics;
import nebula.plugin.metrics.model.TaskExecution;
import nebula.plugin.metrics.time.BuildStartedTime;
import nebula.plugin.metrics.time.Clock;
import org.gradle.BuildAdapter;
import org.gradle.BuildResult;
import org.gradle.StartParameter;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.ProjectEvaluationListener;
import org.gradle.api.ProjectState;
import org.gradle.api.Task;
import org.gradle.api.artifacts.DependencyResolutionListener;
import org.gradle.api.artifacts.ResolvableDependencies;
import org.gradle.api.execution.TaskExecutionListener;
import org.gradle.api.initialization.Settings;
import org.gradle.api.invocation.Gradle;
import org.gradle.api.tasks.TaskState;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
public final class GradleBuildMetricsCollector extends BuildAdapter implements ProjectEvaluationListener, TaskExecutionListener, DependencyResolutionListener {
private static final long TIMEOUT_MS = 5000;
private final Logger logger = MetricsLoggerFactory.getLogger(GradleBuildMetricsCollector.class);
private final Supplier dispatcherSupplier;
private final BuildStartedTime buildStartedTime;
private final Gradle gradle;
private final AtomicBoolean buildProfileComplete = new AtomicBoolean(false);
private final AtomicBoolean buildResultComplete = new AtomicBoolean(false);
public GradleBuildMetricsCollector(Supplier dispatcherSupplier, BuildStartedTime buildStartedTime, Gradle gradle, BuildMetrics buildMetrics, Clock clock) {
checkNotNull(dispatcherSupplier);
checkNotNull(clock);
this.dispatcherSupplier = checkNotNull(dispatcherSupplier);
this.clock = clock;
this.buildMetrics = buildMetrics;
this.buildStartedTime = buildStartedTime;
this.gradle = gradle;
}
private final Clock clock;
private BuildMetrics buildMetrics;
@Override
public void settingsEvaluated(Settings settings) {
checkNotNull(settings);
initializeBuildMetrics();
buildMetrics.setSettingsEvaluated(clock.getCurrentTime());
}
@Override
public void projectsLoaded(Gradle gradle) {
checkNotNull(gradle);
initializeBuildMetrics();
buildMetrics.setProjectsLoaded(clock.getCurrentTime());
}
// ProjectEvaluationListener
@Override
public void beforeEvaluate(Project project) {
initializeBuildMetrics();
long now = clock.getCurrentTime();
buildMetrics.getProjectProfile(project.getPath()).getConfigurationOperation().setStart(now);
}
@Override
public void afterEvaluate(Project project, ProjectState state) {
initializeBuildMetrics();
long now = clock.getCurrentTime();
ProjectMetrics projectMetrics = buildMetrics.getProjectProfile(project.getPath());
projectMetrics.getConfigurationOperation().setFinish(now);
}
// TaskExecutionListener
@Override
public void beforeExecute(Task task) {
initializeBuildMetrics();
long now = clock.getCurrentTime();
Project project = task.getProject();
ProjectMetrics projectMetrics = buildMetrics.getProjectProfile(project.getPath());
projectMetrics.getTaskProfile(task.getPath()).setStart(now);
}
@Override
public void afterExecute(Task task, TaskState state) {
initializeBuildMetrics();
long now = clock.getCurrentTime();
Project project = task.getProject();
ProjectMetrics projectMetrics = buildMetrics.getProjectProfile(project.getPath());
TaskExecution taskExecution = projectMetrics.getTaskProfile(task.getPath());
taskExecution.setFinish(now);
taskExecution.completed(state);
}
@Override
public void beforeResolve(ResolvableDependencies dependencies) {
initializeBuildMetrics();
long now = clock.getCurrentTime();
buildMetrics.getDependencySetProfile(dependencies.getPath()).setStart(now);
}
@Override
public void afterResolve(ResolvableDependencies dependencies) {
initializeBuildMetrics();
long now = clock.getCurrentTime();
buildMetrics.getDependencySetProfile(dependencies.getPath()).setFinish(now);
}
@Override
public void projectsEvaluated(Gradle gradle) {
checkNotNull(gradle);
initializeBuildMetrics();
buildMetrics.setProjectsEvaluated(clock.getCurrentTime());
StartParameter startParameter = gradle.getStartParameter();
checkState(!startParameter.isOffline(), "Collectors should not be registered when Gradle is running offline");
try {
dispatcherSupplier.get().startAsync().awaitRunning(TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (IllegalStateException | TimeoutException e) {
logger.debug("Error while starting metrics dispatcher. Metrics collection disabled. Error message: {}", getRootCauseMessage(e));
return;
}
try {
Project gradleProject = gradle.getRootProject();
String name = gradleProject.getName();
String version = String.valueOf(gradleProject.getVersion());
nebula.plugin.metrics.model.Project project = new nebula.plugin.metrics.model.Project(name, version);
MetricsDispatcher dispatcher = dispatcherSupplier.get();
dispatcher.started(project); // We register this listener after the build has started, so we fire the start event here instead
GradleToolContainer tool = GradleToolContainer.fromGradle(gradle);
Plugin> plugin = getNebulaInfoBrokerPlugin(gradleProject);
if (plugin == null) {
dispatcher.environment(Info.create(tool, gradleProject));
} else {
GradleInfoCollector collector = new GradleInfoCollector(plugin);
dispatcher.environment(Info.create(tool, collector.getSCM(), collector.getCI(), gradleProject));
}
} catch (Exception e) {
logger.error("Unexpected exception in evaluation listener (error message: {})", getRootCauseMessage(e));
}
}
private Map getNebulaInfoBrokerPluginReports(Project gradleProject) {
try {
Plugin> nebulaInfoBrokerPlugin = getNebulaInfoBrokerPlugin(gradleProject);
Method method = nebulaInfoBrokerPlugin.getClass().getDeclaredMethod("buildReports");
return (Map) method.invoke(nebulaInfoBrokerPlugin);
} catch (Exception e) {
return new HashMap<>();
}
}
private Plugin> getNebulaInfoBrokerPlugin(Project gradleProject) {
Plugin> plugin = gradleProject.getPlugins().findPlugin("nebula.info-broker");
if (plugin == null) {
plugin = gradleProject.getPlugins().findPlugin("info-broker");
}
return plugin;
}
/*
* I have separated buildFinished from GradleCollector's BuildAdapter because we need it to be called
* *after* all BuildListeners have completed their BuildFinished cycle. This is because InfoBrokerPlugin
* only allows access to its reports after the BuildFinish cycle has completed.
*/
public void buildFinishedClosure(BuildResult buildResult) {
Throwable failure = buildResult.getFailure();
Result result = failure == null ? Result.success() : Result.failure(failure);
logger.info("Build finished with result " + result);
MetricsDispatcher dispatcher = dispatcherSupplier.get();
dispatcher.result(result);
Map infoBrokerPluginReports = getNebulaInfoBrokerPluginReports(buildResult.getGradle().getRootProject());
if (infoBrokerPluginReports != null) {
for (Map.Entry report : infoBrokerPluginReports.entrySet()) {
dispatcher.report(report.getKey(), report.getValue());
}
}
buildResultComplete.getAndSet(true);
shutdownIfComplete();
}
@Override
public void buildFinished(BuildResult result) {
if(buildMetrics != null) {
buildMetrics.setBuildFinished(clock.getCurrentTime());
buildFinished(buildMetrics);
buildMetrics.setSuccessful(result.getFailure() == null);
buildMetrics = null;
}
}
public void buildFinished(BuildMetrics result) {
checkNotNull(result);
long startupElapsed = result.getElapsedStartup();
long settingsElapsed = result.getElapsedSettings();
long loadingElapsed = result.getElapsedProjectsLoading();
// Initialisation
MetricsDispatcher dispatcher = this.dispatcherSupplier.get();
dispatcher.event("startup", "init", startupElapsed);
long expectedTotal = startupElapsed;
// Configuration
dispatcher.event("settings", "configure", settingsElapsed);
expectedTotal += settingsElapsed;
dispatcher.event("projectsLoading", "configure", loadingElapsed);
expectedTotal += loadingElapsed;
for (ProjectMetrics projectMetrics : result.getProjects()) {
ContinuousOperation configurationOperation = projectMetrics.getConfigurationOperation();
long configurationElapsed = configurationOperation.getElapsedTime();
dispatcher.event(configurationOperation.getDescription(), "configure", configurationElapsed);
expectedTotal += configurationElapsed;
}
// Resolve
for (ContinuousOperation operation : result.getDependencySets()) {
long resolveElapsed = operation.getElapsedTime();
dispatcher.event(operation.getDescription(), "resolve", resolveElapsed);
expectedTotal += resolveElapsed;
}
// Execution
for (ProjectMetrics projectMetrics : result.getProjects()) {
long totalTaskElapsed = 0;
CompositeOperation tasks = projectMetrics.getTasks();
for (TaskExecution execution : tasks.getOperations()) {
Result taskResult = getTaskExecutionResult(execution);
long taskElapsed = execution.getElapsedTime();
nebula.plugin.metrics.model.Task task = new nebula.plugin.metrics.model.Task(execution.getDescription(), taskResult, new DateTime(execution.getStartTime()), taskElapsed);
dispatcher.task(task);
totalTaskElapsed += taskElapsed;
}
dispatcher.event("task", "execution", totalTaskElapsed);
expectedTotal += totalTaskElapsed; // totalTaskElapsed is equal to result.getElapsedTotalExecutionTime()
}
long elapsedTotal = result.getElapsedTotal();
dispatcher.duration(result.getBuildStarted(), elapsedTotal);
// Check the totals agree with the aggregate elapsed times, and log an event with the difference if not
// For instance, Gradle doesn't account for the time taken to download artifacts: http://forums.gradle.org/gradle/topics/profile-report-doesnt-account-for-time-spent-downloading-dependencies
if (elapsedTotal < expectedTotal) {
long difference = expectedTotal - elapsedTotal;
logger.info("Total build time of {}ms is less than the calculated total of {}ms (difference: {}ms). Creating 'unknown' event with type 'other'", expectedTotal, elapsedTotal, difference);
dispatcher.event("unknown", "other", difference);
}
buildProfileComplete.getAndSet(true);
shutdownIfComplete();
}
/**
* Conditionally shutdown the dispatcher, because Gradle listener event order appears to be non-deterministic.
*/
private void shutdownIfComplete() {
// only shut down if you have updated build results AND profile information
if (!buildProfileComplete.get() || !buildResultComplete.get()) {
return;
}
MetricsDispatcher dispatcher = this.dispatcherSupplier.get();
logger.info("Shutting down dispatcher");
try {
dispatcher.stopAsync().awaitTerminated(TIMEOUT_MS, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
logger.debug("Timed out after {}ms while waiting for metrics dispatcher to terminate", TIMEOUT_MS);
} catch (IllegalStateException e) {
logger.debug("Could not stop metrics dispatcher service (error message: {})", getRootCauseMessage(e));
}
Optional receipt = dispatcher.receipt();
if (receipt.isPresent()) {
logger.warn(receipt.get());
}
}
private void initializeBuildMetrics() {
if(buildMetrics != null) {
return;
}
long now = clock.getCurrentTime();
BuildMetrics buildMetrics = new BuildMetrics(gradle.getStartParameter());
buildMetrics.setBuildStarted(buildStartedTime.getStartTime());
buildMetrics.setProfilingStarted(now);
this.buildMetrics = buildMetrics;
}
@VisibleForTesting
Result getTaskExecutionResult(TaskExecution taskExecution) {
Result result = Result.success();
TaskState state = taskExecution.getState();
if (state == null || !state.getDidWork()) {
result = Result.skipped();
} else {
//noinspection ThrowableResultOfMethodCallIgnored
Throwable failure = state.getFailure();
if (failure != null) {
result = Result.failure(failure);
}
}
return result;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy