Skip to content

Commit

Permalink
Implement cquery --output=graph
Browse files Browse the repository at this point in the history
Thankfully, `query` already has most of the infrastructure necessary to make this
easy.

query implements graph output (in `GraphOutputFormatter`) over a
`Digraph<Target>`, which is a generic graph data structure with `Target` payloads.
All output logic then runs over this data structure. To opt query in, all we have
to do is create an equivalent `Digraph<ConfiguredTarget>`, which is a simple
transformation from the backing graph.

This change creates a new generic class for that common logic:
`GraphOutputWriter`. query's `GraphOutputFormatter` then becomes a simple wrapper
over that, and the new `GraphOutputFormatterCallback` is cquery's equivalent.

A few differences:

 - cquery output is always fully ordered (`--order_output=full`). We could match
   this with query's controllable version, but I don't see a reason to make this
   yet another bit to configured.
 - query output annotates edges with select() conditions. cquery doesn't do this
   because select()s are resolved and removed from the graph after analysis. I
   think we could annotate edges with the *chosen* condition if there was
   demand, but that'd be a followup effort.

Fixes #10843 (for `cquery`, not `aquery`)

Closes #12248.

PiperOrigin-RevId: 337907070
  • Loading branch information
gregestren authored and copybara-github committed Oct 19, 2020
1 parent 2300e6d commit 02cbcd2
Show file tree
Hide file tree
Showing 10 changed files with 769 additions and 297 deletions.
19 changes: 17 additions & 2 deletions site/docs/cquery.html
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,22 @@ <h4>--[no]proto:include_configurations</h4>
included as <code>rule_input</code> fields.
</p>

<h3>Graph output</h3>

<pre>
--output=graph
</pre>

<p>
This option generates output as a Graphviz-compatible .dot file. See <code>query</code>'s
<a href="query.html#output-graph">graph output documentation</a> for details. <code>cquery</code>
also supports

<a href="query.html#graph-node_limit-n"><code>--graph:node_limit</code></a> and
<a href="query.html#no-graph-factored"><code>--graph:factored</code></a>.

</p>

<h3>Defining the output format using Starlark</h3>

<pre>
Expand Down Expand Up @@ -608,8 +624,7 @@ <h2 id='known-issues'>Known issues</h2>

<li>
<strong>No support
for <a href="query.html#output-graph"><code>--output=graph</code></a>
or <a href="query.html#output-xml"><code>--output=xml</code></a>.</strong>
for <a href="query.html#output-xml"><code>--output=xml</code></a>.</strong>
</li>

<li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import com.google.devtools.build.lib.query2.engine.QueryEvalResult;
import com.google.devtools.build.lib.query2.engine.QueryException;
import com.google.devtools.build.lib.query2.engine.QueryExpression;
import com.google.devtools.build.lib.query2.engine.QueryUtil;
import com.google.devtools.build.lib.query2.engine.QueryUtil.AggregateAllOutputFormatterCallback;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.QueryRuntimeHelper;
import com.google.devtools.build.lib.runtime.QueryRuntimeHelper.QueryRuntimeHelperException;
Expand All @@ -35,6 +37,7 @@
import com.google.devtools.build.skyframe.WalkableGraph;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;

/**
* Version of {@link BuildTool} that handles all work for queries based on results from the analysis
Expand Down Expand Up @@ -149,11 +152,33 @@ private void doPostAnalysisQuery(
NamedThreadSafeOutputFormatterCallback.callbackNames(callbacks))));
return;
}

// A certain subset of output formatters support "streaming" results - the formatter is called
// multiple times where each call has only a some of the full query results (see
// StreamedOutputFormatter for details). cquery and aquery don't do this. But the reason is
// subtle and hard to follow. Post-analysis output formatters inherit from Callback, which
// declares "void process(Iterable<T> partialResult)". Its javadoc says that the subinterface
// BatchCallback may stream partial results. But post-analysis callbacks don't inherit
// BatchCallback!
//
// To protect against accidental feature regression (like implementing a callback that
// accidentally inherits BatchCallback), we explicitly disable streaming here. The aggregating
// callback collects the entire query's results, even if the query was evaluated in a streaming
// manner. Note that streaming query evaluation is a distinct concept from streaming output
// formatting. Once the complete query finishes, we replay the full results back to the original
// callback. That way callback implementations can safely assume they're only called once and
// the results for that call are indeed complete.
AggregateAllOutputFormatterCallback<T, Set<T>> aggregateResultsCallback =
QueryUtil.newOrderedAggregateAllOutputFormatterCallback(postAnalysisQueryEnvironment);
QueryEvalResult result =
postAnalysisQueryEnvironment.evaluateQuery(queryExpression, callback);
postAnalysisQueryEnvironment.evaluateQuery(queryExpression, aggregateResultsCallback);
if (result.isEmpty()) {
env.getReporter().handle(Event.info("Empty query results"));
}
callback.start();
callback.process(aggregateResultsCallback.getResult());
callback.close(/*failFast=*/ !result.getSuccess());

queryRuntimeHelper.afterQueryOutputIsWritten();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ public class CommonQueryOptions extends OptionsBase {
+ " with top-level options.")
public List<String> universeScope;

@Option(
name = "line_terminator_null",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.QUERY,
effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
help = "Whether each format is terminated with \\0 instead of newline.")
public boolean lineTerminatorNull;

/** Ugly workaround since line terminator option default has to be constant expression. */
public String getLineTerminator() {
if (lineTerminatorNull) {
return "\0";
}

return System.lineSeparator();
}

@Option(
name = "infer_universe_scope",
defaultValue = "false",
Expand Down Expand Up @@ -253,4 +270,30 @@ public AspectResolutionModeConverter() {
+ "precise mode is not completely precise: the decision whether to compute an aspect "
+ "is decided in the analysis phase, which is not run during 'bazel query'.")
public AspectResolver.Mode aspectDeps;

///////////////////////////////////////////////////////////
// GRAPH OUTPUT FORMATTER OPTIONS //
///////////////////////////////////////////////////////////

@Option(
name = "graph:node_limit",
defaultValue = "512",
documentationCategory = OptionDocumentationCategory.QUERY,
effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
help =
"The maximum length of the label string for a graph node in the output. Longer labels"
+ " will be truncated; -1 means no truncation. This option is only applicable to"
+ " --output=graph.")
public int graphNodeStringLimit;

@Option(
name = "graph:factored",
defaultValue = "true",
documentationCategory = OptionDocumentationCategory.QUERY,
effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
help =
"If true, then the graph will be emitted 'factored', i.e. topologically-equivalent nodes "
+ "will be merged together and their labels concatenated. This option is only "
+ "applicable to --output=graph.")
public boolean graphFactored;
}
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ private static ImmutableMap<String, BuildConfiguration> getTransitiveConfigurati
OutputType.JSON),
new BuildOutputFormatterCallback(
eventHandler, cqueryOptions, out, skyframeExecutor, accessor),
new GraphOutputFormatterCallback(
eventHandler,
cqueryOptions,
out,
skyframeExecutor,
accessor,
ct -> getFwdDeps(ImmutableList.of(ct))),
new StarlarkOutputFormatterCallback(
eventHandler, cqueryOptions, out, skyframeExecutor, accessor));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2020 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.query2.cquery;

import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.analysis.ConfiguredTarget;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.events.ExtendedEventHandler;
import com.google.devtools.build.lib.graph.Digraph;
import com.google.devtools.build.lib.graph.Node;
import com.google.devtools.build.lib.query2.engine.QueryEnvironment.TargetAccessor;
import com.google.devtools.build.lib.query2.query.output.GraphOutputWriter;
import com.google.devtools.build.lib.query2.query.output.GraphOutputWriter.NodeReader;
import com.google.devtools.build.lib.skyframe.SkyframeExecutor;
import java.io.OutputStream;
import java.util.Comparator;

/** cquery output formatter that prints the result as factored graph in AT&amp;T GraphViz format. */
class GraphOutputFormatterCallback extends CqueryThreadsafeCallback {
@Override
public String getName() {
return "graph";
}

/** Interface for finding a configured target's direct dependencies. */
@FunctionalInterface
public interface DepsRetriever {
Iterable<ConfiguredTarget> getDirectDeps(ConfiguredTarget taget) throws InterruptedException;
}

private final DepsRetriever depsRetriever;

private final GraphOutputWriter.NodeReader<ConfiguredTarget> nodeReader =
new NodeReader<ConfiguredTarget>() {

private final Comparator<ConfiguredTarget> configuredTargetOrdering =
(ct1, ct2) -> {
// Order graph output first by target label, then by configuration hash.
Label label1 = ct1.getLabel();
Label label2 = ct2.getLabel();
return label1.equals(label2)
? ct1.getConfigurationChecksum().compareTo(ct2.getConfigurationChecksum())
: label1.compareTo(label2);
};

@Override
public String getLabel(Node<ConfiguredTarget> node) {
// Node payloads are ConfiguredTargets. Output node labels are target labels + config
// hashes.
ConfiguredTarget ct = node.getLabel();
return String.format(
"%s (%s)",
ct.getLabel(),
// TODO(gregce): Even if getConfiguration is a cache hit this has overhead, especially
// when called many times on the same configuration in the same query. Investigate the
// performance impact and apply a cache if measurements justify it (also in other
// callbacks that do this).
shortId(skyframeExecutor.getConfiguration(eventHandler, ct.getConfigurationKey())));
}

@Override
public Comparator<ConfiguredTarget> comparator() {
return configuredTargetOrdering;
}
};

GraphOutputFormatterCallback(
ExtendedEventHandler eventHandler,
CqueryOptions options,
OutputStream out,
SkyframeExecutor skyframeExecutor,
TargetAccessor<ConfiguredTarget> accessor,
DepsRetriever depsRetriever) {
super(eventHandler, options, out, skyframeExecutor, accessor);
this.depsRetriever = depsRetriever;
}

@Override
public void processOutput(Iterable<ConfiguredTarget> partialResult) throws InterruptedException {
// Transform the cquery-backed graph into a Digraph to make it suitable for GraphOutputWriter.
// Note that this involves an extra iteration over the entire query result subgraph. We could
// conceptually merge transformation and output writing into the same iteration if needed.
Digraph<ConfiguredTarget> graph = new Digraph<>();
ImmutableSet<ConfiguredTarget> allNodes = ImmutableSet.copyOf(partialResult);
for (ConfiguredTarget configuredTarget : partialResult) {
Node<ConfiguredTarget> node = graph.createNode(configuredTarget);
for (ConfiguredTarget dep : depsRetriever.getDirectDeps(configuredTarget)) {
if (allNodes.contains(dep)) {
Node<ConfiguredTarget> depNode = graph.createNode(dep);
graph.addEdge(node, depNode);
}
}
}

GraphOutputWriter<ConfiguredTarget> graphWriter =
new GraphOutputWriter<>(
nodeReader,
options.getLineTerminator(),
/*sortLabels=*/ true,
options.graphNodeStringLimit,
// select() conditions don't matter for cquery because cquery operates post-analysis
// phase, when select()s have been resolved and removed from the graph.
/*maxConditionalEdges=*/ 0,
options.graphFactored);
graphWriter.write(graph, /*conditionalEdges=*/ null, outputStream);
}
}
Loading

0 comments on commit 02cbcd2

Please sign in to comment.