blob: ed82c145e076f87f4ea76f9619416549ab388db1 [file] [log] [blame]
// 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.skyframe;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.devtools.build.lib.actions.ActionAnalysisMetadata;
import com.google.devtools.build.lib.actions.ActionKeyContext;
import com.google.devtools.build.lib.actions.ActionLookupValue;
import com.google.devtools.build.lib.actions.Actions;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.actions.ArtifactPrefixConflictException;
import com.google.devtools.build.lib.actions.MapBasedActionGraph;
import com.google.devtools.build.lib.actions.MutableActionGraph;
import com.google.devtools.build.lib.actions.MutableActionGraph.ActionConflictException;
import com.google.devtools.build.lib.concurrent.ExecutorUtil;
import com.google.devtools.build.lib.concurrent.Sharder;
import com.google.devtools.build.lib.skyframe.PrecomputedValue.Precomputed;
import com.google.devtools.build.lib.vfs.PathFragment;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.Nullable;
class ArtifactConflictFinder {
static final Precomputed<ImmutableMap<ActionAnalysisMetadata, ConflictException>>
ACTION_CONFLICTS = new Precomputed<>("action_conflicts");
// Action graph construction is CPU-bound.
static final int NUM_JOBS = Runtime.getRuntime().availableProcessors();
private ArtifactConflictFinder() {}
/**
* Find conflicts between generated artifacts. There are two ways to have conflicts. First, if two
* (unshareable) actions generate the same output artifact, this will result in an {@link
* ActionConflictException}. Second, if one action generates an artifact whose path is a prefix of
* another artifact's path, those two artifacts cannot exist simultaneously in the output tree.
* This causes an {@link ArtifactPrefixConflictException}.
*
* <p>This method must be called if a new action was added to the graph this build, so whenever a
* new configured target was analyzed this build. It is somewhat expensive (~1s range for a medium
* build as of 2014), so it should only be called when necessary.
*/
static ActionConflictsAndStats findAndStoreArtifactConflicts(
Sharder<ActionLookupValue> actionLookupValues,
int actionCount,
boolean strictConflictChecks,
ActionKeyContext actionKeyContext)
throws InterruptedException {
ConcurrentMap<ActionAnalysisMetadata, ConflictException> temporaryBadActionMap =
new ConcurrentHashMap<>();
// Use the action count to presize - all actions have at least one output artifact.
MapBasedActionGraph actionGraph = new MapBasedActionGraph(actionKeyContext, actionCount);
List<Artifact> artifacts = new ArrayList<>(actionCount);
constructActionGraphAndArtifactList(
actionGraph,
Collections.synchronizedList(artifacts),
actionLookupValues,
temporaryBadActionMap);
Map<ActionAnalysisMetadata, ArtifactPrefixConflictException> actionsWithArtifactPrefixConflict =
Actions.findArtifactPrefixConflicts(actionGraph, artifacts, strictConflictChecks);
for (Map.Entry<ActionAnalysisMetadata, ArtifactPrefixConflictException> actionExceptionPair :
actionsWithArtifactPrefixConflict.entrySet()) {
temporaryBadActionMap.put(
actionExceptionPair.getKey(), new ConflictException(actionExceptionPair.getValue()));
}
return ActionConflictsAndStats.create(
ImmutableMap.copyOf(temporaryBadActionMap),
actionGraph.getSize());
}
/**
* Simultaneously construct an action graph for all the actions in Skyframe and a map from {@link
* PathFragment}s to their respective {@link Artifact}s. We do this in a threadpool to save around
* 1.5 seconds on a mid-sized build versus a single-threaded operation.
*/
private static void constructActionGraphAndArtifactList(
MutableActionGraph actionGraph,
List<Artifact> artifacts,
Sharder<ActionLookupValue> actionShards,
ConcurrentMap<ActionAnalysisMetadata, ConflictException> badActionMap)
throws InterruptedException {
ExecutorService executor =
Executors.newFixedThreadPool(
NUM_JOBS,
new ThreadFactoryBuilder().setNameFormat("ActionLookupValue Processor %d").build());
for (List<ActionLookupValue> shard : actionShards) {
executor.execute(() -> actionRegistration(shard, actionGraph, artifacts, badActionMap));
}
if (ExecutorUtil.interruptibleShutdown(executor)) {
throw new InterruptedException();
}
}
private static void actionRegistration(
List<ActionLookupValue> values,
MutableActionGraph actionGraph,
List<Artifact> allArtifacts,
ConcurrentMap<ActionAnalysisMetadata, ConflictException> badActionMap) {
// Accumulated and added to the shared list at the end to reduce contention.
List<Artifact> myArtifacts = new ArrayList<>(values.size());
for (ActionLookupValue value : values) {
for (ActionAnalysisMetadata action : value.getActions()) {
try {
actionGraph.registerAction(action);
} catch (ActionConflictException e) {
// It may be possible that we detect a conflict for the same action more than once, if
// that action belongs to multiple aspect values. In this case we will harmlessly
// overwrite the badActionMap entry.
badActionMap.put(action, new ConflictException(e));
// We skip the rest of the loop, and do not add the path->artifact mapping for this
// artifact below -- we don't need to check it since this action is already in
// error.
continue;
} catch (InterruptedException e) {
// Bail.
Thread.currentThread().interrupt();
return;
}
myArtifacts.addAll(action.getOutputs());
}
}
allArtifacts.addAll(myArtifacts);
}
/**
* A typed union of {@link ActionConflictException}, which indicates two actions that generate the
* same {@link Artifact}, and {@link ArtifactPrefixConflictException}, which indicates that the
* path of one {@link Artifact} is a prefix of another.
*/
static class ConflictException extends Exception {
@Nullable private final ActionConflictException ace;
@Nullable private final ArtifactPrefixConflictException apce;
ConflictException(ActionConflictException e) {
super(e);
this.ace = e;
this.apce = null;
}
ConflictException(ArtifactPrefixConflictException e) {
super(e);
this.ace = null;
this.apce = e;
}
IllegalStateException rethrowTyped()
throws ActionConflictException, ArtifactPrefixConflictException {
if (ace == null) {
throw Preconditions.checkNotNull(apce);
}
if (apce == null) {
throw Preconditions.checkNotNull(ace);
}
throw new IllegalStateException("malformed ConflictException has no well-typed cause");
}
}
@AutoValue
abstract static class ActionConflictsAndStats {
abstract ImmutableMap<ActionAnalysisMetadata, ConflictException> getConflicts();
abstract int getOutputArtifactCount();
private static ActionConflictsAndStats create(
ImmutableMap<ActionAnalysisMetadata, ConflictException> conflicts,
int artifactCount) {
return new AutoValue_ArtifactConflictFinder_ActionConflictsAndStats(conflicts, artifactCount);
}
}
}