Bug 530649 - Make BuildManager able to process build graph in parallel

Implements a parallel graph navigation with Jobs.

Change-Id: Ibfcc12ac2d8c6c39eaca4649a98d72d7e50e2a1f
Signed-off-by: Mickael Istria <mistria@redhat.com>
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java
index 1b2c196..9d90df5 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/BuildManager.java
@@ -19,6 +19,7 @@
 import java.util.stream.Collectors;
 import org.eclipse.core.internal.dtree.DeltaDataTree;
 import org.eclipse.core.internal.resources.*;
+import org.eclipse.core.internal.resources.ComputeProjectOrder.Digraph;
 import org.eclipse.core.internal.utils.Messages;
 import org.eclipse.core.internal.utils.Policy;
 import org.eclipse.core.internal.watson.ElementTree;
@@ -103,7 +104,7 @@
 
 	//the job for performing background autobuild
 	final AutoBuildJob autoBuildJob;
-	private final Set<IProject> builtProjects = new HashSet<>();
+	private final Set<IProject> builtProjects = Collections.synchronizedSet(new HashSet<>());
 
 	//the following four fields only apply for the lifetime of a single builder invocation.
 	protected final Set<InternalBuilder> currentBuilders;
@@ -392,6 +393,55 @@
 	}
 
 	/**
+	 * Runs all builders on all the given project configs, in the order that
+	 * they are given.
+	 * @param buildJobGroup
+	 * @return A status indicating if the build succeeded or failed
+	 */
+	public IStatus buildParallel(Digraph<IBuildConfiguration> configs, IBuildConfiguration[] requestedConfigs, int trigger, JobGroup buildJobGroup, IProgressMonitor monitor) {
+		monitor = Policy.monitorFor(monitor);
+		try {
+			monitor.beginTask(Messages.events_building_0, TOTAL_BUILD_WORK);
+			try {
+				builtProjects.clear();
+				hookStartBuild(configs.vertexList.stream().map(vertex -> vertex.id).toArray(IBuildConfiguration[]::new), trigger);
+				MultiStatus status = new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.BUILD_FAILED, Messages.events_errors, null);
+				parallelBuildLoop(configs, requestedConfigs, trigger, buildJobGroup, status, monitor);
+				return status;
+			} finally {
+				hookEndBuild(trigger);
+			}
+		} finally {
+			monitor.done();
+			if (trigger == IncrementalProjectBuilder.INCREMENTAL_BUILD || trigger == IncrementalProjectBuilder.FULL_BUILD)
+				autoBuildJob.avoidBuild();
+		}
+	}
+
+	private void parallelBuildLoop(final Digraph<IBuildConfiguration> configs, IBuildConfiguration[] requestedConfigs, int trigger, JobGroup buildJobGroup, MultiStatus status, IProgressMonitor monitor) {
+		final int projectWork = configs.vertexList.size() > 0 ? TOTAL_BUILD_WORK / configs.vertexList.size() : 0;
+		builtProjects.clear();
+		final GraphProcessor<IBuildConfiguration> graphProcessor = new GraphProcessor<>(configs, IBuildConfiguration.class, (config, graphCrawler) -> {
+			IBuildContext context = new BuildContext(config, requestedConfigs, graphCrawler.getSequentialOrder()); // TODO consider passing Digraph to BuildConfig?
+			try {
+				workspace.prepareOperation(null, monitor);
+				workspace.beginOperation(false);
+				basicBuild(config, trigger, context, status, Policy.subMonitorFor(monitor, projectWork));
+				workspace.endOperation(null, false);
+				builtProjects.add(config.getProject());
+			} catch (CoreException ex) {
+				status.add(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, ex.getMessage(), ex));
+			}
+		}, buildJobGroup);
+		graphProcessor.processGraphWithParallelJobs();
+		try {
+			Job.getJobManager().join(graphProcessor, monitor);
+		} catch (OperationCanceledException | InterruptedException e) {
+			status.add(new Status(IStatus.ERROR, ResourcesPlugin.PI_RESOURCES, e.getMessage(), e));
+		}
+	}
+
+	/**
 	 * Runs the builder with the given name on the given project config.
 	 * @return A status indicating if the build succeeded or failed
 	 */
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/GraphProcessor.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/GraphProcessor.java
new file mode 100644
index 0000000..d3fbba6
--- /dev/null
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/events/GraphProcessor.java
@@ -0,0 +1,135 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Red Hat Inc. and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *   Mickael Istria (Red Hat Inc.)
+ *******************************************************************************/
+package org.eclipse.core.internal.events;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import org.eclipse.core.internal.resources.ComputeProjectOrder;
+import org.eclipse.core.internal.resources.ComputeProjectOrder.Digraph;
+import org.eclipse.core.internal.resources.ComputeProjectOrder.Digraph.Edge;
+import org.eclipse.core.internal.resources.ComputeProjectOrder.VertexOrder;
+import org.eclipse.core.runtime.*;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.jobs.JobGroup;
+
+/**
+ *
+ */
+class GraphProcessor<T> {
+
+	final private Digraph<T> graph;
+	final private Set<T> toProcess;
+	final private Set<T> processing;
+	final private Set<T> processed;
+	final private VertexOrder<T> sequentialOrder;
+	final private JobGroup buildJobGroup;
+	final BiConsumer<T, GraphProcessor<T>> processor;
+
+	GraphProcessor(Digraph<T> graph1, Class<T> clazz, final BiConsumer<T, GraphProcessor<T>> processor, JobGroup buildJobGroup) {
+		this.graph = graph1;
+		this.processor = processor;
+		this.buildJobGroup = buildJobGroup;
+		toProcess = new HashSet<>(graph.vertexMap.keySet());
+		processing = new HashSet<>();
+		processed = new HashSet<>();
+		sequentialOrder = ComputeProjectOrder.computeVertexOrder(graph, clazz);
+	}
+
+	private boolean complete() {
+		return processed.size() == graph.vertexList.size();
+	}
+
+	private boolean allTriggered() {
+		return toProcess.isEmpty();
+	}
+
+	private void markProcessing(T item) {
+		if (!toProcess.remove(item)) {
+			throw new IllegalArgumentException();
+		}
+		processing.add(item);
+	}
+
+	void markProcessed(T item) {
+		if (!processing.remove(item)) {
+			throw new IllegalArgumentException();
+		}
+		processed.add(item);
+	}
+
+	private Set<T> computeReadyVertexes() {
+		Set<T> res = new HashSet<>(toProcess);
+		for (T item : toProcess) {
+			for (Edge<T> edge : graph.getEdges()) {
+				if (edge.to == item && !processed.contains(edge.from)) {
+					res.remove(item);
+				}
+			}
+		}
+		if (res.isEmpty() && !isProcessing()) { // nothing ready, nothing running: a cycle!
+			for (T id : sequentialOrder.vertexes) {
+				if (!isProcessed(id)) {
+					return Collections.singleton(id);
+				}
+			}
+		}
+		return res;
+	}
+
+	private boolean isProcessing() {
+		return !processing.isEmpty();
+	}
+
+	private boolean isProcessed(T item) {
+		return processed.contains(item);
+	}
+
+	public T[] getSequentialOrder() {
+		return this.sequentialOrder.vertexes;
+	}
+
+	public void processGraphWithParallelJobs() {
+		if (!complete()) {
+			if (!allTriggered()) {
+				Set<T> readyToBuild = computeReadyVertexes();
+				readyToBuild.forEach(item -> triggerJob(item));
+			}
+		}
+	}
+
+	private void triggerJob(T item) {
+		synchronized (this) {
+			markProcessing(item);
+		}
+		Job buildJob = new Job(item.toString()) {
+
+			@Override
+			protected IStatus run(IProgressMonitor monitor) {
+				processor.accept(item, GraphProcessor.this);
+				synchronized (GraphProcessor.this) {
+					markProcessed(item);
+					// do it as part of Job so we're sure following jobs are triggered before this one completes,
+					// so we can safely rely on join(family)
+					processGraphWithParallelJobs();
+				}
+				return Status.OK_STATUS;
+			}
+
+			@Override
+			public boolean belongsTo(Object family) {
+				return super.belongsTo(family) || family == GraphProcessor.this;
+			}
+		};
+		buildJob.setJobGroup(buildJobGroup);
+		buildJob.schedule();
+	}
+
+}
\ No newline at end of file