diff options
author | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
---|---|---|
committer | Han-Wen Nienhuys <hanwen@google.com> | 2015-02-25 16:45:20 +0100 |
commit | d08b27fa9701fecfdb69e1b0d1ac2459efc2129b (patch) | |
tree | 5d50963026239ca5aebfb47ea5b8db7e814e57c8 /src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java |
Update from Google.
--
MOE_MIGRATED_REVID=85702957
Diffstat (limited to 'src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java')
-rw-r--r-- | src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java b/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java new file mode 100644 index 0000000000..7abf6c6f8a --- /dev/null +++ b/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java @@ -0,0 +1,350 @@ +// Copyright 2014 Google Inc. 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.ThreadSafety.ThreadSafe; +import com.google.devtools.build.lib.util.Pair; + +import java.util.Set; +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 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 DirtiableGraph graph; + @Nullable protected final EvaluationProgressReceiver invalidationReceiver; + protected final DirtyKeyTracker dirtyKeyTracker; + // Aliased to InvalidationState.pendingVisitations. + protected final Set<Pair<SkyKey, InvalidationType>> pendingVisitations; + + protected InvalidatingNodeVisitor( + DirtiableGraph graph, @Nullable EvaluationProgressReceiver invalidationReceiver, + InvalidationState state, DirtyKeyTracker dirtyKeyTracker) { + super(/*concurrent*/true, + /*corePoolSize*/DEFAULT_THREAD_COUNT, + /*maxPoolSize*/DEFAULT_THREAD_COUNT, + 1, TimeUnit.SECONDS, + /*failFastOnException*/true, + /*failFastOnInterrupt*/true, + "skyframe-invalidator"); + 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); + } + + protected void informInvalidationReceiver(SkyValue value, + EvaluationProgressReceiver.InvalidationState state) { + if (invalidationReceiver != null && value != null) { + invalidationReceiver.invalidated(value, 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 { + + 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 + 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); + } + } + + if (entry.isDone()) { + // Only process this node's value and children if it is done, since dirty nodes have + // no awareness of either. + + // Unregister this node from direct deps, since reverse dep edges cannot point to + // non-existent nodes. + if (traverseGraph) { + for (SkyKey directDep : entry.getDirectDeps()) { + NodeEntry dep = graph.get(directDep); + if (dep != null) { + dep.removeReverseDep(key); + } + } + } + // Allow custom Value-specific logic to update dirtiness status. + informInvalidationReceiver(entry.getValue(), + EvaluationProgressReceiver.InvalidationState.DELETED); + } + if (traverseGraph) { + // Force reverseDeps consolidation (validates that attempts to remove reverse deps were + // really successful. + entry.getReverseDeps(); + } + // 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 { + + private final Set<Pair<SkyKey, InvalidationType>> visited = Sets.newConcurrentHashSet(); + + protected DirtyingNodeVisitor(DirtiableGraph graph, + EvaluationProgressReceiver invalidationReceiver, InvalidationState state, + DirtyKeyTracker dirtyKeyTracker) { + super(graph, invalidationReceiver, state, dirtyKeyTracker); + } + + /** + * 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() { + NodeEntry 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; + } + + // This entry remains in the graph in this dirty state until it is re-evaluated. + Pair<? extends Iterable<SkyKey>, ? extends SkyValue> depsAndValue = + entry.markDirty(isChanged); + // It is not safe to interrupt the logic from this point until the end of the method. + // Any exception thrown should be unrecoverable. + if (depsAndValue == null) { + // 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); + } + + // Remove this node as a reverse dep from its children, since we have reset it and it no + // longer lists its children as direct deps. + for (SkyKey dep : depsAndValue.first) { + graph.get(dep).removeReverseDep(key); + } + + SkyValue value = ValueWithMetadata.justValue(depsAndValue.second); + informInvalidationReceiver(value, EvaluationProgressReceiver.InvalidationState.DIRTY); + dirtyKeyTracker.dirty(key); + // Remove the node from the set as the last operation. + pendingVisitations.remove(invalidationPair); + } + }); + } + } +} |