Skip to content

Commit

Permalink
Maven-like rolling output when the build happens to be linear #269
Browse files Browse the repository at this point in the history
  • Loading branch information
ppalaga committed Dec 16, 2020
1 parent 38b7185 commit c3ac2f6
Show file tree
Hide file tree
Showing 6 changed files with 910 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ private boolean doAccept(Message entry) {
this.maxThreads = bs.getMaxThreads();
final int maxThreadsDigits = (int) (Math.log10(maxThreads) + 1);
this.threadsFormat = "%" + (maxThreadsDigits * 3 + 2) + "s";
if (maxThreads <= 1 || totalProjects <= 1) {
this.noBuffering = true;
display.update(Collections.emptyList(), 0);
applyNoBuffering();
}
break;
}
case Message.CANCEL_BUILD: {
Expand Down Expand Up @@ -322,8 +327,7 @@ private boolean doAccept(Message entry) {
case CTRL_B:
noBuffering = !noBuffering;
if (noBuffering) {
projects.values().stream().flatMap(p -> p.log.stream()).forEach(log);
projects.clear();
applyNoBuffering();
} else {
clearDisplay();
}
Expand All @@ -345,6 +349,11 @@ private boolean doAccept(Message entry) {
return true;
}

private void applyNoBuffering() {
projects.values().stream().flatMap(p -> p.log.stream()).forEach(log);
projects.clear();
}

@Override
public void describeTerminal() {
StringBuilder sb = new StringBuilder();
Expand Down
290 changes: 270 additions & 20 deletions daemon/src/main/java/org/mvndaemon/mvnd/builder/DependencyGraph.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,151 @@
*/
package org.mvndaemon.mvnd.builder;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
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.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.execution.ProjectDependencyGraph;
import org.apache.maven.project.MavenProject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* File origin:
* https://github.com/takari/takari-smart-builder/blob/takari-smart-builder-0.6.1/src/main/java/io/takari/maven/builder/smart/DependencyGraph.java
*/
interface DependencyGraph<K> {
public class DependencyGraph<K> {

private static final Logger logger = LoggerFactory.getLogger(DependencyGraph.class);
static final Pattern mvndRuleSanitizerPattern = Pattern.compile("[,\\s]+");

private final List<K> projects;
private final Map<K, List<K>> upstreams;
private final Map<K, List<K>> downstreams;

public static DependencyGraph<MavenProject> fromMaven(MavenSession session) {

final ProjectDependencyGraph graph = session.getProjectDependencyGraph();
final List<MavenProject> projects = graph.getSortedProjects();
return fromMaven(graph, getRules(projects, session));
}

static String getRules(List<MavenProject> projects, MavenSession session) {
List<String> list = new ArrayList<>();

String providerScript = null;
final MavenProject topLevelProject = projects.get(0);
String providerUrl = topLevelProject.getProperties()
.getProperty(SmartBuilder.MVND_BUILDER_RULES_PROVIDER_URL);
if (providerUrl != null) {
logger.warn(SmartBuilder.MVND_BUILDER_RULES_PROVIDER_URL
+ " property is deprecated and the support for it will be removed in mvnd 0.3. See https://github.com/mvndaemon/mvnd/issues/264");

URL url;
try {
url = new URL(providerUrl);
} catch (MalformedURLException e) {
try {
url = new File(providerUrl).toURI().toURL();
} catch (MalformedURLException ex) {
url = null;
}
}
if (url == null) {
throw new RuntimeException("Bad syntax for " + SmartBuilder.MVND_BUILDER_RULES_PROVIDER_URL, null);
}
try (BufferedReader r = new BufferedReader(new InputStreamReader(url.openStream()))) {
StringBuilder sb = new StringBuilder();
char[] buf = new char[8192];
int l;
while ((l = r.read(buf)) >= 0) {
sb.append(buf, 0, l);
}
providerScript = sb.toString();
} catch (IOException e) {
throw new RuntimeException("Unable to read provider url " + SmartBuilder.MVND_BUILDER_RULES_PROVIDER_URL,
e);
}
}
if (providerScript == null) {
providerScript = topLevelProject.getProperties()
.getProperty(SmartBuilder.MVND_BUILDER_RULES_PROVIDER_SCRIPT);
}
if (providerScript != null) {
logger.warn(SmartBuilder.MVND_BUILDER_RULES_PROVIDER_SCRIPT
+ " property is deprecated and the support for it will be removed in mvnd 0.3. See https://github.com/mvndaemon/mvnd/issues/264");

Binding binding = new Binding();
GroovyShell shell = new GroovyShell(binding);
binding.setProperty("session", session);
Object result = shell.evaluate(providerScript);
if (result instanceof Iterable) {
for (Object r : (Iterable) result) {
list.add(r.toString());
}
} else if (result != null) {
list.add(result.toString());
} else {
throw new RuntimeException("The provider script did not return a valid string or string collection", null);
}
list.add(result.toString());
}

String topRule = topLevelProject.getProperties().getProperty(SmartBuilder.MVND_BUILDER_RULES);
if (topRule != null) {
logger.warn(SmartBuilder.MVND_BUILDER_RULES
+ " property is deprecated and the support for it will be removed in mvnd 0.3. See https://github.com/mvndaemon/mvnd/issues/264");
list.add(topRule);
}

projects.forEach(p -> {
String rule = p.getProperties().getProperty(SmartBuilder.MVND_BUILDER_RULE);
if (rule != null) {
logger.warn(SmartBuilder.MVND_BUILDER_RULE
+ " property is deprecated and the support for it will be removed in mvnd 0.3. See https://github.com/mvndaemon/mvnd/issues/264");
rule = rule.trim();
if (!rule.isEmpty()) {
rule = mvndRuleSanitizerPattern.matcher(rule).replaceAll(",");
list.add(rule + " before " + p.getGroupId() + ":" + p.getArtifactId());
}
}
});
String rules = null;
if (!list.isEmpty()) {
rules = String.join("\n", list);
}
return rules;
}

static DependencyGraph<MavenProject> fromMaven(ProjectDependencyGraph graph, String rules) {
List<MavenProject> projects = graph.getSortedProjects();
final List<MavenProject> projects = graph.getSortedProjects();
Map<MavenProject, List<MavenProject>> upstreams = projects.stream()
.collect(Collectors.toMap(p -> p, p -> graph.getUpstreamProjects(p, false)));
Map<MavenProject, List<MavenProject>> downstreams = projects.stream()
.collect(Collectors.toMap(p -> p, p -> graph.getDownstreamProjects(p, false)));
.collect(
Collectors.toMap(p -> p, p -> graph.getDownstreamProjects(p, false)));

if (rules != null) {
for (String rule : rules.split("\\s*;\\s*|\n")) {
Expand Down Expand Up @@ -80,35 +203,162 @@ static DependencyGraph<MavenProject> fromMaven(ProjectDependencyGraph graph, Str
deps.get(0).forEach(p -> downstreams.get(p).addAll(deps.get(1)));
}
}
return new DependencyGraph<MavenProject>() {
@Override
public Stream<MavenProject> getDownstreamProjects(MavenProject project) {
return downstreams.get(project).stream();
return new DependencyGraph<MavenProject>(Collections.unmodifiableList(projects), upstreams, downstreams);
}

public DependencyGraph(List<K> projects, Map<K, List<K>> upstreams, Map<K, List<K>> downstreams) {
this.projects = projects;
this.upstreams = upstreams;
this.downstreams = downstreams;
}

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

public Stream<K> getUpstreamProjects(K project) {
return upstreams.get(project).stream();
}

public boolean isRoot(K project) {
return upstreams.get(project).isEmpty();
}

public Stream<K> getProjects() {
return projects.stream();
}

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

public 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);
}
}

static 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;
}

@Override
public Stream<MavenProject> getProjects() {
return projects.stream();
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 Stream<MavenProject> getUpstreamProjects(MavenProject project) {
return upstreams.get(project).stream();
public boolean hasNext() {
return !nexts.isEmpty();
}

@Override
public boolean isRoot(MavenProject project) {
return upstreams.get(project).isEmpty();
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());
}

Stream<K> getProjects();
/**
* 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));
}

boolean isRoot(K project);
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());
}

Stream<K> getDownstreamProjects(K project);
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;
}

Stream<K> getUpstreamProjects(K project);
}

}
Loading

0 comments on commit c3ac2f6

Please sign in to comment.