Skip to content

Commit

Permalink
Compute dependency graph width
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet authored and ppalaga committed Dec 16, 2020
1 parent 2708c12 commit fe963d5
Show file tree
Hide file tree
Showing 7 changed files with 820 additions and 15 deletions.
138 changes: 138 additions & 0 deletions daemon/src/main/java/org/mvndaemon/mvnd/builder/DagWidth.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2020 the original author or authors.
*
* 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 org.mvndaemon.mvnd.builder;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class DagWidth<K> {

private final DependencyGraph<K> graph;
private final Map<K, Set<K>> allUpstreams = new HashMap<>();

public DagWidth(DependencyGraph<K> graph) {
this.graph = graph;
graph.getProjects().forEach(this::allUpstreams);
}

public int getMaxWidth() {
return getMaxWidth(Integer.MAX_VALUE);
}

public int getMaxWidth(int maxmax) {
return getMaxWidth(maxmax, Long.MAX_VALUE);
}

public int getMaxWidth(int maxmax, long maxTimeMillis) {
int max = 0;
if (maxmax < allUpstreams.size()) {
// try inverted upstream bound
Map<Set<K>, Set<K>> mapByUpstreams = new HashMap<>();
allUpstreams.forEach((k, ups) -> {
mapByUpstreams.computeIfAbsent(ups, n -> new HashSet<>()).add(k);
});
max = mapByUpstreams.values().stream()
.mapToInt(Set::size)
.max()
.orElse(0);
if (max >= maxmax) {
return maxmax;
}
}
long tmax = System.currentTimeMillis() + maxTimeMillis;
int tries = 0;
SubsetIterator iterator = new SubsetIterator(getRoots());
while (max < maxmax && iterator.hasNext()) {
if (++tries % 100 == 0 && System.currentTimeMillis() < tmax) {
return maxmax;
}
List<K> l = iterator.next();
max = Math.max(max, l.size());
}
return max;
}

private class SubsetIterator implements Iterator<List<K>> {

final List<List<K>> nexts = new ArrayList<>();
final Set<List<K>> visited = new HashSet<>();

public SubsetIterator(List<K> roots) {
nexts.add(roots);
}

@Override
public boolean hasNext() {
return !nexts.isEmpty();
}

@Override
public List<K> next() {
List<K> list = nexts.remove(0);
list.stream()
.map(node -> ensembleWithChildrenOf(list, node))
.filter(visited::add)
.forEach(nexts::add);
return list;
}
}

private List<K> getRoots() {
return graph.getProjects()
.filter(graph::isRoot)
.collect(Collectors.toList());
}

/**
* Get a stream of all subset of descendants of the given nodes
*/
private Stream<List<K>> childEnsembles(List<K> list) {
return Stream.concat(
Stream.of(list),
list.parallelStream()
.map(node -> ensembleWithChildrenOf(list, node))
.flatMap(this::childEnsembles));
}

List<K> ensembleWithChildrenOf(List<K> list, K node) {
return Stream.concat(
list.stream().filter(k -> !Objects.equals(k, node)),
graph.getDownstreamProjects(node)
.filter(k -> allUpstreams(k).stream().noneMatch(k2 -> !Objects.equals(k2, node) && list.contains(k2))))
.distinct().collect(Collectors.toList());
}

private Set<K> allUpstreams(K node) {
Set<K> aups = allUpstreams.get(node);
if (aups == null) {
aups = Stream.concat(
graph.getUpstreamProjects(node),
graph.getUpstreamProjects(node).map(this::allUpstreams).flatMap(Set::stream))
.collect(Collectors.toSet());
allUpstreams.put(node, aups);
}
return aups;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
*/
package org.mvndaemon.mvnd.builder;

import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand All @@ -31,4 +37,25 @@ interface DependencyGraph<K> {

Stream<K> getUpstreamProjects(K project);

default int computeMaxWidth(int max, long maxTimeMillis) {
return new DagWidth<>(this).getMaxWidth(max, maxTimeMillis);
}

default void store(Function<K, String> toString, Path path) {
try (Writer w = Files.newBufferedWriter(path)) {
getProjects().forEach(k -> {
try {
w.write(toString.apply(k));
w.write(" = ");
w.write(getUpstreamProjects(k).map(toString).collect(Collectors.joining(",")));
w.write(System.lineSeparator());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ public synchronized void build(final MavenSession session, final ReactorContext
logger.info("Build maximum degree of concurrency is " + degreeOfConcurrency);
logger.info("Total number of projects is " + graph.getAllProjects().size());

// find out the max concurrency
long t0 = System.currentTimeMillis();
long t1 = System.currentTimeMillis();
logger.warn("Project graph width: {} (computed in {} ms)", maxWidth, t1 - t0);

// the actual build execution
List<Map.Entry<TaskSegment, ReactorBuildStats>> allstats = new ArrayList<>();
for (TaskSegment taskSegment : taskSegments) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ public class SmartProjectDependencyGraph implements ProjectDependencyGraph, Depe
private final Map<MavenProject, List<MavenProject>> upstreams;
private final Map<MavenProject, List<MavenProject>> downstreams;
private final ProjectDependencyGraph delegate;
private final int width;

public static SmartProjectDependencyGraph fromMaven(MavenSession session) {

Expand Down Expand Up @@ -194,17 +193,14 @@ static SmartProjectDependencyGraph fromMaven(ProjectDependencyGraph graph, Strin
deps.get(0).forEach(p -> downstreams.get(p).addAll(deps.get(1)));
}
}
final boolean serial = !projects.stream().anyMatch(p -> downstreams.get(p).size() > 1);
final int width = serial ? 1 : Integer.MAX_VALUE;
return new SmartProjectDependencyGraph(Collections.unmodifiableList(projects), upstreams, downstreams, width, graph);
return new SmartProjectDependencyGraph(Collections.unmodifiableList(projects), upstreams, downstreams, graph);
}

public SmartProjectDependencyGraph(List<MavenProject> sortedProjects, Map<MavenProject, List<MavenProject>> upstreams,
Map<MavenProject, List<MavenProject>> downstreams, int width, ProjectDependencyGraph delegate) {
Map<MavenProject, List<MavenProject>> downstreams, ProjectDependencyGraph delegate) {
this.sortedProjects = sortedProjects;
this.upstreams = upstreams;
this.downstreams = downstreams;
this.width = width;
this.delegate = delegate;
}

Expand Down Expand Up @@ -240,14 +236,6 @@ public List<MavenProject> getUpstreamProjects(MavenProject project, boolean tran
return delegate.getUpstreamProjects(project, transitive);
}

/**
* @return the maximum possible concurrency level at which this graph can be built or {@link Integer#MAX_VALUE} if
* it cannot be computed
*/
public int getWidth() {
return width;
}

@Override
public Stream<MavenProject> getProjects() {
return sortedProjects.stream();
Expand Down
3 changes: 2 additions & 1 deletion daemon/src/main/java/org/mvndaemon/mvnd/daemon/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,8 @@ public void fail(Throwable t) throws Exception {
@Override
protected void onStartSession(MavenSession session) {
final SmartProjectDependencyGraph smartGraph = (SmartProjectDependencyGraph) session.getProjectDependencyGraph();
final int maxThreads = Math.min(smartGraph.getWidth(), session.getRequest().getDegreeOfConcurrency());
final int degreeOfConcurrency = session.getRequest().getDegreeOfConcurrency();
final int maxThreads = degreeOfConcurrency == 1 ? 1 : smartGraph.computeMaxWidth(degreeOfConcurrency, 1000);
queue.add(new BuildStarted(getCurrentProject(session).getName(), smartGraph.getSortedProjects().size(),
maxThreads));
}
Expand Down
149 changes: 149 additions & 0 deletions daemon/src/test/java/org/mvndaemon/mvnd/builder/DagWidthTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Copyright 2020 the original author or authors.
*
* 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 org.mvndaemon.mvnd.builder;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class DagWidthTest {

@Test
void testSimpleGraph() {
//
// A B
// / | \ / \
// C D E F
// \/
// G
Map<String, List<String>> upstreams = new HashMap<>();
upstreams.put("A", Collections.emptyList());
upstreams.put("B", Collections.emptyList());
upstreams.put("C", Collections.singletonList("A"));
upstreams.put("D", Collections.singletonList("A"));
upstreams.put("E", Arrays.asList("A", "B"));
upstreams.put("F", Collections.singletonList("B"));
upstreams.put("G", Arrays.asList("D", "E"));
DependencyGraph<String> graph = new SimpleGraph<>(upstreams);

assertEquals(4, new DagWidth<>(graph).getMaxWidth(12));
}

@Test
void testSingle() {
//
// A
//
Map<String, List<String>> upstreams = new HashMap<>();
upstreams.put("A", Collections.emptyList());
DependencyGraph<String> graph = new SimpleGraph<>(upstreams);

assertEquals(1, new DagWidth<>(graph).getMaxWidth(12));
}

@Test
void testLinear() {
//
// A -> B -> C -> D
//
Map<String, List<String>> upstreams = new HashMap<>();
upstreams.put("A", Collections.emptyList());
upstreams.put("B", Collections.singletonList("A"));
upstreams.put("C", Collections.singletonList("B"));
upstreams.put("D", Collections.singletonList("C"));
DependencyGraph<String> graph = new SimpleGraph<>(upstreams);

assertEquals(1, new DagWidth<>(graph).getMaxWidth(12));
}

@Test
public void testHugeGraph() throws IOException {
Map<String, List<String>> upstreams = new HashMap<>();
try (BufferedReader r = new BufferedReader(
new InputStreamReader(getClass().getResourceAsStream("huge-graph.properties")))) {
r.lines().forEach(l -> {
int idxEq = l.indexOf(" = ");
if (!l.startsWith("#") && idxEq > 0) {
String k = l.substring(0, idxEq).trim();
String[] ups = l.substring(idxEq + 3).trim().split(",");
List<String> list = Stream.of(ups).map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
upstreams.put(k, list);
}
});
}
SimpleGraph<String> graph = new SimpleGraph<>(upstreams);

DagWidth<String> w = new DagWidth<>(graph);
List<String> d = w.ensembleWithChildrenOf(graph.downstreams.get("org.apache.camel:camel"),
"org.apache.camel:camel-parent");

assertEquals(12, w.getMaxWidth(12));
}

static class SimpleGraph<K> implements DependencyGraph<K> {

final List<K> nodes;
final Map<K, List<K>> upstreams;
final Map<K, List<K>> downstreams;

public SimpleGraph(Map<K, List<K>> upstreams) {
this.upstreams = upstreams;
this.nodes = Stream.concat(upstreams.keySet().stream(), upstreams.values().stream().flatMap(List::stream))
.distinct()
.sorted()
.collect(Collectors.toList());
this.downstreams = this.nodes.stream().collect(Collectors.toMap(k -> k, k -> new ArrayList<>()));
upstreams.forEach((k, ups) -> {
ups.forEach(up -> downstreams.get(up).add(k));
});
}

@Override
public Stream<K> getProjects() {
return nodes.stream();
}

@Override
public Stream<K> getDownstreamProjects(K project) {
return downstreams.get(project).stream();
}

@Override
public Stream<K> getUpstreamProjects(K project) {
List<K> ups = upstreams.get(project);
return Objects.requireNonNull(ups, () -> "Could not find upstreams for " + project).stream();
}

@Override
public boolean isRoot(K project) {
throw new UnsupportedOperationException();
}
}
}
Loading

0 comments on commit fe963d5

Please sign in to comment.