blob: 2b4fa85c71e721b764e60cc1c98620a471bf5776 [file] [log] [blame]
// Copyright 2014 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.skyframe;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.concurrent.AbstractQueueVisitor;
import com.google.devtools.build.lib.concurrent.ThreadPoolExecutorParams;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.util.Pair;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
/**
* A visitor that is useful for invalidating transitive dependencies of Skyframe nodes.
*
* <p>Interruptibility: It is safe to interrupt the invalidation process at any time. Consider a
* graph and a set of modified nodes. Then the reverse transitive closure of the modified nodes is
* the set of dirty nodes. We provide interruptibility by making sure that the following invariant
* holds at any time:
*
* <p>If a node is dirty, but not removed (or marked as dirty) yet, then either it or any of its
* transitive dependencies must be in the {@link #pendingVisitations} set. Furthermore, reverse dep
* pointers must always point to existing nodes.
*
* <p>Thread-safety: This class should only be instantiated and called on a single thread, but
* internally it spawns many worker threads to process the graph. The thread-safety of the workers
* on the graph can be delicate, and is documented below. Moreover, no other modifications to the
* graph can take place while invalidation occurs.
*
* <p>This is intended only for use in alternative {@code MemoizingEvaluator} implementations.
*/
public abstract class InvalidatingNodeVisitor<TGraph extends ThinNodeQueryableGraph>
extends AbstractQueueVisitor {
// Default thread count is equal to the number of cores to exploit
// that level of hardware parallelism, since invalidation should be CPU-bound.
// We may consider increasing this in the future.
private static final int DEFAULT_THREAD_COUNT = Runtime.getRuntime().availableProcessors();
private static final boolean MUST_EXIST = true;
protected final TGraph graph;
@Nullable protected final EvaluationProgressReceiver invalidationReceiver;
protected final DirtyKeyTracker dirtyKeyTracker;
// Aliased to InvalidationState.pendingVisitations.
protected final Set<Pair<SkyKey, InvalidationType>> pendingVisitations;
protected InvalidatingNodeVisitor(
TGraph graph,
@Nullable EvaluationProgressReceiver invalidationReceiver,
InvalidationState state,
DirtyKeyTracker dirtyKeyTracker) {
this(graph, invalidationReceiver, state, dirtyKeyTracker, EXECUTOR_FACTORY);
}
protected InvalidatingNodeVisitor(
TGraph graph,
@Nullable EvaluationProgressReceiver invalidationReceiver,
InvalidationState state,
DirtyKeyTracker dirtyKeyTracker,
Function<ThreadPoolExecutorParams, ThreadPoolExecutor> executorFactory) {
super(/*concurrent=*/true,
/*corePoolSize=*/DEFAULT_THREAD_COUNT,
/*maxPoolSize=*/DEFAULT_THREAD_COUNT,
/*keepAliveTime=*/1,
/*units=*/TimeUnit.SECONDS,
/*failFastOnException=*/true,
/*failFastOnInterrupt=*/true,
"skyframe-invalidator",
executorFactory);
this.graph = Preconditions.checkNotNull(graph);
this.invalidationReceiver = invalidationReceiver;
this.dirtyKeyTracker = Preconditions.checkNotNull(dirtyKeyTracker);
this.pendingVisitations = state.pendingValues;
}
/**
* Initiates visitation and waits for completion.
*/
void run() throws InterruptedException {
// Make a copy to avoid concurrent modification confusing us as to which nodes were passed by
// the caller, and which are added by other threads during the run. Since no tasks have been
// started yet (the queueDirtying calls start them), this is thread-safe.
for (Pair<SkyKey, InvalidationType> visitData : ImmutableList.copyOf(pendingVisitations)) {
// The caller may have specified non-existent SkyKeys, or there may be stale SkyKeys in
// pendingVisitations that have already been deleted. In both these cases, the nodes will not
// exist in the graph, so we must be tolerant of that case.
visit(visitData.first, visitData.second, !MUST_EXIST);
}
work(/*failFastOnInterrupt=*/true);
Preconditions.checkState(pendingVisitations.isEmpty(),
"All dirty nodes should have been processed: %s", pendingVisitations);
}
@Override
protected boolean isCriticalError(Throwable e) {
return e instanceof RuntimeException;
}
protected abstract long count();
protected void informInvalidationReceiver(SkyKey key,
EvaluationProgressReceiver.InvalidationState state) {
if (invalidationReceiver != null) {
invalidationReceiver.invalidated(key, state);
}
}
/**
* Enqueues a node for invalidation.
*/
@ThreadSafe
abstract void visit(SkyKey key, InvalidationType second, boolean mustExist);
@VisibleForTesting
enum InvalidationType {
/**
* The node is dirty and must be recomputed.
*/
CHANGED,
/**
* The node is dirty, but may be marked clean later during change pruning.
*/
DIRTIED,
/**
* The node is deleted.
*/
DELETED;
}
/**
* Invalidation state object that keeps track of which nodes need to be invalidated, but have not
* been dirtied/deleted yet. This supports interrupts - by only deleting a node from this set
* when all its parents have been invalidated, we ensure that no information is lost when an
* interrupt comes in.
*/
static class InvalidationState {
private final Set<Pair<SkyKey, InvalidationType>> pendingValues = Sets.newConcurrentHashSet();
private final InvalidationType defaultUpdateType;
private InvalidationState(InvalidationType defaultUpdateType) {
this.defaultUpdateType = Preconditions.checkNotNull(defaultUpdateType);
}
void update(Iterable<SkyKey> diff) {
Iterables.addAll(pendingValues, Iterables.transform(diff,
new Function<SkyKey, Pair<SkyKey, InvalidationType>>() {
@Override
public Pair<SkyKey, InvalidationType> apply(SkyKey skyKey) {
return Pair.of(skyKey, defaultUpdateType);
}
}));
}
@VisibleForTesting
boolean isEmpty() {
return pendingValues.isEmpty();
}
@VisibleForTesting
Set<Pair<SkyKey, InvalidationType>> getInvalidationsForTesting() {
return ImmutableSet.copyOf(pendingValues);
}
}
public static class DirtyingInvalidationState extends InvalidationState {
public DirtyingInvalidationState() {
super(InvalidationType.CHANGED);
}
}
static class DeletingInvalidationState extends InvalidationState {
public DeletingInvalidationState() {
super(InvalidationType.DELETED);
}
}
/**
* A node-deleting implementation.
*/
static class DeletingNodeVisitor extends InvalidatingNodeVisitor<DirtiableGraph> {
private final Set<SkyKey> visitedValues = Sets.newConcurrentHashSet();
private final boolean traverseGraph;
protected DeletingNodeVisitor(DirtiableGraph graph,
EvaluationProgressReceiver invalidationReceiver, InvalidationState state,
boolean traverseGraph, DirtyKeyTracker dirtyKeyTracker) {
super(graph, invalidationReceiver, state, dirtyKeyTracker);
this.traverseGraph = traverseGraph;
}
@Override
protected long count() {
return visitedValues.size();
}
@Override
public void visit(final SkyKey key, InvalidationType invalidationType, boolean mustExist) {
Preconditions.checkState(invalidationType == InvalidationType.DELETED, key);
if (!visitedValues.add(key)) {
return;
}
final Pair<SkyKey, InvalidationType> invalidationPair = Pair.of(key, invalidationType);
pendingVisitations.add(invalidationPair);
enqueue(
new Runnable() {
@Override
public void run() {
NodeEntry entry = graph.get(key);
if (entry == null) {
pendingVisitations.remove(invalidationPair);
return;
}
if (traverseGraph) {
// Propagate deletion upwards.
for (SkyKey reverseDep : entry.getReverseDeps()) {
visit(reverseDep, InvalidationType.DELETED, !MUST_EXIST);
}
// Unregister this node as an rdep from its direct deps, since reverse dep edges
// cannot point to non-existent nodes. To know whether the child has this node as an
// "in-progress" rdep to be signaled, or just as a known rdep, we look at the deps
// that this node declared during its last (presumably interrupted) evaluation. If a
// dep is in this set, then it was notified to signal this node, and so the rdep
// will be an in-progress rdep, if the dep itself isn't done. Otherwise it will be a
// normal rdep. That information is used to remove this node as an rdep from the
// correct list of rdeps in the child -- because of our compact storage of rdeps,
// checking which list contains this parent could be expensive.
Set<SkyKey> signalingDeps =
entry.isDone() ? ImmutableSet.<SkyKey>of() : entry.getTemporaryDirectDeps();
Iterable<SkyKey> directDeps =
entry.isDone()
? entry.getDirectDeps()
: entry.getAllDirectDepsForIncompleteNode();
Map<SkyKey, NodeEntry> depMap = graph.getBatch(directDeps);
for (Map.Entry<SkyKey, NodeEntry> directDepEntry : depMap.entrySet()) {
NodeEntry dep = directDepEntry.getValue();
if (dep != null) {
if (dep.isDone() || !signalingDeps.contains(directDepEntry.getKey())) {
dep.removeReverseDep(key);
} else {
// This step is not strictly necessary, since all in-progress nodes are
// deleted during graph cleaning, which happens in a single
// DeletingNodeVisitor visitation, aka the one right now. We leave this here
// in case the logic changes.
dep.removeInProgressReverseDep(key);
}
}
}
}
// Allow custom key-specific logic to update dirtiness status.
informInvalidationReceiver(key, EvaluationProgressReceiver.InvalidationState.DELETED);
// Actually remove the node.
graph.remove(key);
dirtyKeyTracker.notDirty(key);
// Remove the node from the set as the last operation.
pendingVisitations.remove(invalidationPair);
}
});
}
}
/**
* A node-dirtying implementation.
*/
static class DirtyingNodeVisitor extends InvalidatingNodeVisitor<ThinNodeQueryableGraph> {
private final Set<Pair<SkyKey, InvalidationType>> visited = Sets.newConcurrentHashSet();
protected DirtyingNodeVisitor(
ThinNodeQueryableGraph graph,
EvaluationProgressReceiver invalidationReceiver,
InvalidationState state,
DirtyKeyTracker dirtyKeyTracker,
Function<ThreadPoolExecutorParams, ThreadPoolExecutor> executorFactory) {
super(graph, invalidationReceiver, state, dirtyKeyTracker, executorFactory);
}
@Override
protected long count() {
return visited.size();
}
/**
* Queues a task to dirty the node named by {@code key}. May be called from multiple threads.
* It is possible that the same node is enqueued many times. However, we require that a node
* is only actually marked dirty/changed once, with two exceptions:
*
* (1) If a node is marked dirty, it can subsequently be marked changed. This can occur if, for
* instance, FileValue workspace/foo/foo.cc is marked dirty because FileValue workspace/foo is
* marked changed (and every FileValue depends on its parent). Then FileValue
* workspace/foo/foo.cc is itself changed (this can even happen on the same build).
*
* (2) If a node is going to be marked both dirty and changed, as, for example, in the previous
* case if both workspace/foo/foo.cc and workspace/foo have been changed in the same build, the
* thread marking workspace/foo/foo.cc dirty may race with the one marking it changed, and so
* try to mark it dirty after it has already been marked changed. In that case, the
* {@link NodeEntry} ignores the second marking.
*
* The invariant that we do not process a (SkyKey, InvalidationType) pair twice is enforced by
* the {@link #visited} set.
*
* The "invariant" is also enforced across builds by checking to see if the entry is already
* marked changed, or if it is already marked dirty and we are just going to mark it dirty
* again.
*
* If either of the above tests shows that we have already started a task to mark this entry
* dirty/changed, or that it is already marked dirty/changed, we do not continue this task.
*/
@Override
@ThreadSafe
public void visit(final SkyKey key, final InvalidationType invalidationType,
final boolean mustExist) {
Preconditions.checkState(invalidationType != InvalidationType.DELETED, key);
final boolean isChanged = (invalidationType == InvalidationType.CHANGED);
final Pair<SkyKey, InvalidationType> invalidationPair = Pair.of(key, invalidationType);
if (!visited.add(invalidationPair)) {
return;
}
pendingVisitations.add(invalidationPair);
enqueue(
new Runnable() {
@Override
public void run() {
ThinNodeEntry entry = graph.get(key);
if (entry == null) {
Preconditions.checkState(
!mustExist,
"%s does not exist in the graph but was enqueued for dirtying by another node",
key);
pendingVisitations.remove(invalidationPair);
return;
}
if (entry.isChanged() || (!isChanged && entry.isDirty())) {
// If this node is already marked changed, or we are only marking this node
// dirty, and it already is, move along.
pendingVisitations.remove(invalidationPair);
return;
}
// It is not safe to interrupt the logic from this point until the end of the method.
// Any exception thrown should be unrecoverable.
// This entry remains in the graph in this dirty state until it is re-evaluated.
if (!entry.markDirty(isChanged)) {
// Another thread has already dirtied this node. Don't do anything in this thread.
pendingVisitations.remove(invalidationPair);
return;
}
// Propagate dirtiness upwards and mark this node dirty/changed. Reverse deps should
// only be marked dirty (because only a dependency of theirs has changed).
for (SkyKey reverseDep : entry.getReverseDeps()) {
visit(reverseDep, InvalidationType.DIRTIED, MUST_EXIST);
}
informInvalidationReceiver(key, EvaluationProgressReceiver.InvalidationState.DIRTY);
dirtyKeyTracker.dirty(key);
// Remove the node from the set as the last operation.
pendingVisitations.remove(invalidationPair);
}
});
}
}
}