// Copyright 2015 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; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Ascii; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet.Builder; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import com.google.devtools.build.lib.cmdline.Label; import com.google.devtools.build.lib.cmdline.LabelSyntaxException; import com.google.devtools.build.lib.cmdline.PackageIdentifier; import com.google.devtools.build.lib.cmdline.TargetParsingException; import com.google.devtools.build.lib.cmdline.TargetPattern; import com.google.devtools.build.lib.collect.CompactHashSet; import com.google.devtools.build.lib.concurrent.NamedForkJoinPool; import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe; import com.google.devtools.build.lib.events.Event; import com.google.devtools.build.lib.events.EventHandler; import com.google.devtools.build.lib.graph.Digraph; import com.google.devtools.build.lib.packages.BuildFileContainsErrorsException; import com.google.devtools.build.lib.packages.DependencyFilter; import com.google.devtools.build.lib.packages.NoSuchTargetException; import com.google.devtools.build.lib.packages.NoSuchThingException; import com.google.devtools.build.lib.packages.Package; import com.google.devtools.build.lib.packages.Rule; import com.google.devtools.build.lib.packages.Target; import com.google.devtools.build.lib.pkgcache.PathPackageLocator; import com.google.devtools.build.lib.pkgcache.TargetPatternEvaluator; import com.google.devtools.build.lib.profiler.AutoProfiler; import com.google.devtools.build.lib.query2.engine.AllRdepsFunction; import com.google.devtools.build.lib.query2.engine.Callback; import com.google.devtools.build.lib.query2.engine.FunctionExpression; 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.QueryExpressionEvalListener; import com.google.devtools.build.lib.query2.engine.QueryExpressionMapper; import com.google.devtools.build.lib.query2.engine.QueryUtil.AbstractThreadSafeUniquifier; import com.google.devtools.build.lib.query2.engine.RdepsFunction; import com.google.devtools.build.lib.query2.engine.StreamableQueryEnvironment; import com.google.devtools.build.lib.query2.engine.TargetLiteral; import com.google.devtools.build.lib.query2.engine.ThreadSafeCallback; import com.google.devtools.build.lib.query2.engine.ThreadSafeUniquifier; import com.google.devtools.build.lib.query2.engine.Uniquifier; import com.google.devtools.build.lib.query2.engine.VariableContext; import com.google.devtools.build.lib.skyframe.BlacklistedPackagePrefixesValue; import com.google.devtools.build.lib.skyframe.ContainingPackageLookupFunction; import com.google.devtools.build.lib.skyframe.FileValue; import com.google.devtools.build.lib.skyframe.GraphBackedRecursivePackageProvider; import com.google.devtools.build.lib.skyframe.PackageLookupValue; import com.google.devtools.build.lib.skyframe.PackageValue; import com.google.devtools.build.lib.skyframe.PrepareDepsOfPatternsFunction; import com.google.devtools.build.lib.skyframe.RecursivePackageProviderBackedTargetPatternResolver; import com.google.devtools.build.lib.skyframe.SkyFunctions; import com.google.devtools.build.lib.skyframe.TargetPatternValue; import com.google.devtools.build.lib.skyframe.TargetPatternValue.TargetPatternKey; import com.google.devtools.build.lib.skyframe.TransitiveTraversalValue; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.vfs.PathFragment; import com.google.devtools.build.lib.vfs.RootedPath; import com.google.devtools.build.skyframe.EvaluationResult; import com.google.devtools.build.skyframe.InterruptibleSupplier; import com.google.devtools.build.skyframe.SkyFunctionName; import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; import com.google.devtools.build.skyframe.WalkableGraph; import com.google.devtools.build.skyframe.WalkableGraph.WalkableGraphFactory; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Queue; import java.util.Set; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Logger; import javax.annotation.Nullable; /** * {@link AbstractBlazeQueryEnvironment} that introspects the Skyframe graph to find forward and * reverse edges. Results obtained by calling {@link #evaluateQuery} are not guaranteed to be in any * particular order. As well, this class eagerly loads the full transitive closure of targets, even * if the full closure isn't needed. */ public class SkyQueryEnvironment extends AbstractBlazeQueryEnvironment implements StreamableQueryEnvironment { // 10k is likely a good balance between using batch efficiently and not blowing up memory. // TODO(janakr): Unify with RecursivePackageProviderBackedTargetPatternResolver's constant. static final int BATCH_CALLBACK_SIZE = 10000; protected static final int DEFAULT_THREAD_COUNT = Runtime.getRuntime().availableProcessors(); private static final int MAX_QUERY_EXPRESSION_LOG_CHARS = 1000; private static final Logger LOG = Logger.getLogger(SkyQueryEnvironment.class.getName()); private static final Function TARGET_LABEL_FUNCTION = new Function() { @Override public Label apply(Target target) { return target.getLabel(); } }; private final BlazeTargetAccessor accessor = new BlazeTargetAccessor(this); private final int loadingPhaseThreads; private final WalkableGraphFactory graphFactory; private final List universeScope; protected final String parserPrefix; private final PathPackageLocator pkgPath; private final int queryEvaluationParallelismLevel; // The following fields are set in the #beforeEvaluateQuery method. protected WalkableGraph graph; private InterruptibleSupplier> blacklistPatternsSupplier; private ForkJoinPool forkJoinPool; private RecursivePackageProviderBackedTargetPatternResolver resolver; public SkyQueryEnvironment( boolean keepGoing, int loadingPhaseThreads, EventHandler eventHandler, Set settings, Iterable extraFunctions, QueryExpressionEvalListener evalListener, String parserPrefix, WalkableGraphFactory graphFactory, List universeScope, PathPackageLocator pkgPath) { this( keepGoing, loadingPhaseThreads, // SkyQueryEnvironment operates on a prepopulated Skyframe graph. Therefore, query // evaluation is completely CPU-bound. /*queryEvaluationParallelismLevel=*/ DEFAULT_THREAD_COUNT, eventHandler, settings, extraFunctions, evalListener, parserPrefix, graphFactory, universeScope, pkgPath); } protected SkyQueryEnvironment( boolean keepGoing, int loadingPhaseThreads, int queryEvaluationParallelismLevel, EventHandler eventHandler, Set settings, Iterable extraFunctions, QueryExpressionEvalListener evalListener, String parserPrefix, WalkableGraphFactory graphFactory, List universeScope, PathPackageLocator pkgPath) { super( keepGoing, /*strictScope=*/ true, /*labelFilter=*/ Rule.ALL_LABELS, eventHandler, settings, extraFunctions, evalListener); this.loadingPhaseThreads = loadingPhaseThreads; this.graphFactory = graphFactory; this.pkgPath = pkgPath; this.universeScope = Preconditions.checkNotNull(universeScope); this.parserPrefix = parserPrefix; Preconditions.checkState( !universeScope.isEmpty(), "No queries can be performed with an empty universe"); this.queryEvaluationParallelismLevel = queryEvaluationParallelismLevel; } private void beforeEvaluateQuery() throws InterruptedException { EvaluationResult result; try (AutoProfiler p = AutoProfiler.logged("evaluation and walkable graph", LOG)) { result = graphFactory.prepareAndGet( universeScope, parserPrefix, loadingPhaseThreads, eventHandler); } SkyKey universeKey = graphFactory.getUniverseKey(universeScope, parserPrefix); checkEvaluationResult(result, universeKey); graph = result.getWalkableGraph(); blacklistPatternsSupplier = InterruptibleSupplier.Memoize.of(new BlacklistSupplier(graph)); ImmutableList universeTargetPatternKeys = PrepareDepsOfPatternsFunction.getTargetPatternKeys( PrepareDepsOfPatternsFunction.getSkyKeys(universeKey, eventHandler)); GraphBackedRecursivePackageProvider graphBackedRecursivePackageProvider = new GraphBackedRecursivePackageProvider(graph, universeTargetPatternKeys, pkgPath); forkJoinPool = NamedForkJoinPool.newNamedPool("QueryEnvironment", queryEvaluationParallelismLevel); resolver = new RecursivePackageProviderBackedTargetPatternResolver( graphBackedRecursivePackageProvider, eventHandler, TargetPatternEvaluator.DEFAULT_FILTERING_POLICY, forkJoinPool); } /** * The {@link EvaluationResult} is from the evaluation of a single PrepareDepsOfPatterns node. We * expect to see either a single successfully evaluated value or a cycle in the result. */ private void checkEvaluationResult(EvaluationResult result, SkyKey universeKey) { Collection values = result.values(); if (!values.isEmpty()) { Preconditions.checkState( values.size() == 1, "Universe query \"%s\" returned multiple values unexpectedly (%s values in result)", universeScope, values.size()); Preconditions.checkNotNull(result.get(universeKey), result); } else { // No values in the result, so there must be an error. We expect the error to be a cycle. boolean foundCycle = !Iterables.isEmpty(result.getError().getCycleInfo()); Preconditions.checkState( foundCycle, "Universe query \"%s\" failed with non-cycle error: %s", universeScope, result.getError()); } } /** * A {@link QueryExpressionMapper} that transforms each occurrence of an expression of the form * {@literal 'rdeps(, )'} to {@literal 'allrdeps()'}. The latter is more * efficient. */ protected static class RdepsToAllRdepsQueryExpressionMapper extends QueryExpressionMapper { protected final TargetPattern.Parser targetPatternParser; private final String absoluteUniverseScopePattern; protected RdepsToAllRdepsQueryExpressionMapper( TargetPattern.Parser targetPatternParser, String universeScopePattern) { this.targetPatternParser = targetPatternParser; this.absoluteUniverseScopePattern = targetPatternParser.absolutize(universeScopePattern); } @Override public QueryExpression map(FunctionExpression functionExpression) { if (functionExpression.getFunction().getName().equals(new RdepsFunction().getName())) { List args = functionExpression.getArgs(); QueryExpression universeExpression = args.get(0).getExpression(); if (universeExpression instanceof TargetLiteral) { TargetLiteral literalUniverseExpression = (TargetLiteral) universeExpression; String absolutizedUniverseExpression = targetPatternParser.absolutize(literalUniverseExpression.getPattern()); if (absolutizedUniverseExpression.equals(absoluteUniverseScopePattern)) { List argsTail = args.subList(1, functionExpression.getArgs().size()); return new FunctionExpression(new AllRdepsFunction(), argsTail); } } } return super.map(functionExpression); } } @Override public final QueryExpression transformParsedQuery(QueryExpression queryExpression) { QueryExpression transformedQueryExpression = getTransformedQueryExpression(queryExpression); LOG.info(String.format( "transformed query [%s] to [%s]", Ascii.truncate( queryExpression.toString(), MAX_QUERY_EXPRESSION_LOG_CHARS, "[truncated]"), Ascii.truncate( transformedQueryExpression.toString(), MAX_QUERY_EXPRESSION_LOG_CHARS, "[truncated]"))); return transformedQueryExpression; } protected QueryExpression getTransformedQueryExpression(QueryExpression queryExpression) { if (universeScope.size() != 1) { return queryExpression; } TargetPattern.Parser targetPatternParser = new TargetPattern.Parser(parserPrefix); String universeScopePattern = Iterables.getOnlyElement(universeScope); return queryExpression.getMapped( new RdepsToAllRdepsQueryExpressionMapper(targetPatternParser, universeScopePattern)); } @Override public QueryEvalResult evaluateQuery(QueryExpression expr, Callback callback) throws QueryException, InterruptedException { // Some errors are reported as QueryExceptions and others as ERROR events (if --keep_going). The // result is set to have an error iff there were errors emitted during the query, so we reset // errors here. eventHandler.resetErrors(); beforeEvaluateQuery(); // SkyQueryEnvironment batches callback invocations using a BatchStreamedCallback, created here // so that there's one per top-level evaluateQuery call. The batch size is large enough that // per-call costs of calling the original callback are amortized over a good number of targets, // and small enough that holding a batch of targets in memory doesn't risk an OOM error. // // This flushes the batched callback prior to constructing the QueryEvalResult in the unlikely // case of a race between the original callback and the eventHandler. final BatchStreamedCallback aggregator = new BatchStreamedCallback(callback, BATCH_CALLBACK_SIZE); final AtomicBoolean empty = new AtomicBoolean(true); ThreadSafeCallback callbackWithEmptyCheck = new ThreadSafeCallback() { @Override public void process(Iterable partialResult) throws QueryException, InterruptedException { empty.compareAndSet(true, Iterables.isEmpty(partialResult)); aggregator.process(partialResult); } }; try (final AutoProfiler p = AutoProfiler.logged("evaluating query", LOG)) { try { eval(expr, VariableContext.empty(), callbackWithEmptyCheck); } catch (QueryException e) { throw new QueryException(e, expr); } finally { // Force termination of remaining tasks - if evaluateQuery was successful there should be // none, if it failed abruptly (e.g. was interrupted) we don't want to leave any dangling // threads running tasks. forkJoinPool.shutdownNow(); forkJoinPool.awaitQuiescence(Long.MAX_VALUE, TimeUnit.MILLISECONDS); } aggregator.processLastPending(); } if (eventHandler.hasErrors()) { if (!keepGoing) { // This case represents loading-phase errors reported during evaluation // of target patterns that don't cause evaluation to fail per se. throw new QueryException( "Evaluation of query \"" + expr + "\" failed due to BUILD file errors"); } else { eventHandler.handle( Event.warn("--keep_going specified, ignoring errors. " + "Results may be inaccurate")); } } return new QueryEvalResult(!eventHandler.hasErrors(), empty.get()); } private Map> makeTargetsMap(Map> input) throws InterruptedException { ImmutableMap.Builder> result = ImmutableMap.builder(); Map allTargets = makeTargetsFromSkyKeys(Sets.newHashSet(Iterables.concat(input.values()))); for (Map.Entry entry : makeTargetsFromSkyKeys(input.keySet()).entrySet()) { Iterable skyKeys = input.get(entry.getKey()); Set targets = CompactHashSet.createWithExpectedSize(Iterables.size(skyKeys)); for (SkyKey key : skyKeys) { Target target = allTargets.get(key); if (target != null) { targets.add(target); } } result.put(entry.getValue(), targets); } return result.build(); } private Map> getRawFwdDeps(Iterable targets) throws InterruptedException { return makeTargetsMap(graph.getDirectDeps(makeTransitiveTraversalKeys(targets))); } private Map> getRawReverseDeps(Iterable targets) throws InterruptedException { return makeTargetsMap(graph.getReverseDeps(makeTransitiveTraversalKeys(targets))); } private Set