Don't remove reverse deps until node is known to be changed. This helps avoid mutating the deps of nodes that are still going to be deps after evaluation is finished.
--
MOS_MIGRATED_REVID=103659429
diff --git a/src/main/java/com/google/devtools/build/skyframe/BuildingState.java b/src/main/java/com/google/devtools/build/skyframe/BuildingState.java
index 547549e..41b94ad 100644
--- a/src/main/java/com/google/devtools/build/skyframe/BuildingState.java
+++ b/src/main/java/com/google/devtools/build/skyframe/BuildingState.java
@@ -73,9 +73,10 @@
/**
* The state of a dirty node. A node is marked dirty in the BuildingState constructor, and goes
- * into either the state {@link DirtyState#CHECK_DEPENDENCIES} or {@link DirtyState#REBUILDING},
- * depending on whether the caller specified that the node was itself changed or not. A non-null
- * {@code dirtyState} indicates that the node {@link #isDirty} in some way.
+ * into either the state {@link DirtyState#CHECK_DEPENDENCIES} or
+ * {@link DirtyState#NEEDS_REBUILDING}, depending on whether the caller specified that the node
+ * was itself changed or not. A non-null {@code dirtyState} indicates that the node
+ * {@link #isDirty} in some way.
*/
private DirtyState dirtyState = null;
@@ -118,41 +119,41 @@
// node will be removed by the time evaluation starts, so reverse deps to signal can just be
// reverse deps in the main ValueEntry object.
private Object reverseDepsToSignal = ImmutableList.of();
- private List<SkyKey> reverseDepsToRemove = null;
+ private Object reverseDepsDataToConsolidate = null;
private boolean reverseDepIsSingleObject = false;
private static final ReverseDepsUtil<BuildingState> REVERSE_DEPS_UTIL =
new ReverseDepsUtil<BuildingState>() {
- @Override
- void setReverseDepsObject(BuildingState container, Object object) {
- container.reverseDepsToSignal = object;
- }
+ @Override
+ void setReverseDepsObject(BuildingState container, Object object) {
+ container.reverseDepsToSignal = object;
+ }
- @Override
- void setSingleReverseDep(BuildingState container, boolean singleObject) {
- container.reverseDepIsSingleObject = singleObject;
- }
+ @Override
+ void setSingleReverseDep(BuildingState container, boolean singleObject) {
+ container.reverseDepIsSingleObject = singleObject;
+ }
- @Override
- void setReverseDepsToRemove(BuildingState container, List<SkyKey> object) {
- container.reverseDepsToRemove = object;
- }
+ @Override
+ void setDataToConsolidate(BuildingState container, Object dataToConsolidate) {
+ container.reverseDepsDataToConsolidate = dataToConsolidate;
+ }
- @Override
- Object getReverseDepsObject(BuildingState container) {
- return container.reverseDepsToSignal;
- }
+ @Override
+ Object getReverseDepsObject(BuildingState container) {
+ return container.reverseDepsToSignal;
+ }
- @Override
- boolean isSingleReverseDep(BuildingState container) {
- return container.reverseDepIsSingleObject;
- }
+ @Override
+ boolean isSingleReverseDep(BuildingState container) {
+ return container.reverseDepIsSingleObject;
+ }
- @Override
- List<SkyKey> getReverseDepsToRemove(BuildingState container) {
- return container.reverseDepsToRemove;
- }
- };
+ @Override
+ Object getDataToConsolidate(BuildingState container) {
+ return container.reverseDepsDataToConsolidate;
+ }
+ };
// Below are fields that are used for dirty nodes.
@@ -180,12 +181,10 @@
Preconditions.checkState(isChanged || !this.lastBuildDirectDeps.isEmpty(),
"%s is being marked dirty, not changed, but has no children that could have dirtied it",
this);
- dirtyState = isChanged ? DirtyState.REBUILDING
- : DirtyState.CHECK_DEPENDENCIES;
- if (dirtyState == DirtyState.CHECK_DEPENDENCIES) {
- // We need to iterate through the deps to see if they have changed. Initialize the iterator.
- dirtyDirectDepIterator = lastBuildDirectDeps.iterator();
- }
+ dirtyState = isChanged ? DirtyState.NEEDS_REBUILDING : DirtyState.CHECK_DEPENDENCIES;
+ // We need to iterate through the deps to see if they have changed, or to remove them if one
+ // has. Initialize the iterator.
+ dirtyDirectDepIterator = lastBuildDirectDeps.iterator();
}
static BuildingState newDirtyState(boolean isChanged,
@@ -197,7 +196,7 @@
Preconditions.checkState(isDirty(), this);
Preconditions.checkState(!isChanged(), this);
Preconditions.checkState(!evaluating, this);
- dirtyState = DirtyState.REBUILDING;
+ dirtyState = DirtyState.NEEDS_REBUILDING;
}
void forceChanged() {
@@ -234,11 +233,7 @@
* @see NodeEntry#isChanged()
*/
boolean isChanged() {
- return dirtyState == DirtyState.REBUILDING;
- }
-
- private boolean rebuilding() {
- return dirtyState == DirtyState.REBUILDING;
+ return dirtyState == DirtyState.NEEDS_REBUILDING || dirtyState == DirtyState.REBUILDING;
}
/**
@@ -246,10 +241,14 @@
* actually like to check that the node has been evaluated, but that is not available in
* this context.
*/
- private void checkNotProcessing() {
+ private void checkFinishedBuildingWhenAboutToSetValue() {
Preconditions.checkState(evaluating, "not started building %s", this);
- Preconditions.checkState(!isDirty() || dirtyState == DirtyState.VERIFIED_CLEAN
- || rebuilding(), "not done building %s", this);
+ Preconditions.checkState(
+ !isDirty()
+ || dirtyState == DirtyState.VERIFIED_CLEAN
+ || dirtyState == DirtyState.REBUILDING,
+ "not done building %s",
+ this);
Preconditions.checkState(isReady(), "not done building %s", this);
}
@@ -269,7 +268,7 @@
* finished is equal to the number of known children.
*
* <p>If the node is dirty and checking its deps for changes, this also updates {@link
- * #dirtyState} as needed -- {@link DirtyState#REBUILDING} if the child has changed,
+ * #dirtyState} as needed -- {@link DirtyState#NEEDS_REBUILDING} if the child has changed,
* and {@link DirtyState#VERIFIED_CLEAN} if the child has not changed and this was the
* last child to be checked (as determined by {@link #dirtyDirectDepIterator}.hasNext() and
* isReady()).
@@ -278,15 +277,15 @@
*/
boolean signalDep(boolean childChanged) {
signaledDeps++;
- if (isDirty() && !rebuilding()) {
- // Synchronization isn't needed here because the only caller is ValueEntry, which does it
- // through the synchronized method signalDep(long).
+ if (isDirty() && !isChanged()) {
+ // Synchronization isn't needed here because the only caller is NodeEntry, which does it
+ // through the synchronized method signalDep(Version).
if (childChanged) {
- dirtyState = DirtyState.REBUILDING;
+ dirtyState = DirtyState.NEEDS_REBUILDING;
} else if (dirtyState == DirtyState.CHECK_DEPENDENCIES
&& isReady()
&& !dirtyDirectDepIterator.hasNext()) {
- // No other dep already marked this as REBUILDING, no deps outstanding, and this was
+ // No other dep already marked this as NEEDS_REBUILDING, no deps outstanding, and this was
// the last block of deps to be checked.
dirtyState = DirtyState.VERIFIED_CLEAN;
}
@@ -300,7 +299,7 @@
* was built, in the same order. Should only be used by {@link NodeEntry#setValue}.
*/
boolean unchangedFromLastBuild(SkyValue newValue) {
- checkNotProcessing();
+ checkFinishedBuildingWhenAboutToSetValue();
return getLastBuildValue().equals(newValue) && lastBuildDirectDeps.equals(directDeps);
}
@@ -341,6 +340,22 @@
return nextDeps;
}
+ Collection<SkyKey> getAllRemainingDirtyDirectDeps() {
+ Preconditions.checkState(isDirty(), this);
+ ImmutableList.Builder<SkyKey> result = ImmutableList.builder();
+ while (dirtyDirectDepIterator.hasNext()) {
+ result.addAll(dirtyDirectDepIterator.next());
+ }
+ return result.build();
+ }
+
+ Collection<SkyKey> markRebuildingAndGetAllRemainingDirtyDirectDeps() {
+ Preconditions.checkState(dirtyState == DirtyState.NEEDS_REBUILDING, this);
+ Collection<SkyKey> result = getAllRemainingDirtyDirectDeps();
+ dirtyState = DirtyState.REBUILDING;
+ return result;
+ }
+
void addDirectDeps(GroupedListHelper<SkyKey> depsThisRun) {
directDeps.append(depsThisRun);
}
@@ -380,7 +395,7 @@
* @see NodeEntry#addReverseDepAndCheckIfDone(SkyKey)
*/
void addReverseDepToSignal(SkyKey newReverseDep) {
- REVERSE_DEPS_UTIL.consolidateReverseDepsRemovals(this);
+ REVERSE_DEPS_UTIL.consolidateData(this);
REVERSE_DEPS_UTIL.addReverseDeps(this, Collections.singleton(newReverseDep));
}
diff --git a/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java
index ad50bea..9e346eb 100644
--- a/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java
+++ b/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java
@@ -27,7 +27,6 @@
import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DeletingInvalidationState;
import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.DirtyingInvalidationState;
import com.google.devtools.build.skyframe.InvalidatingNodeVisitor.InvalidationState;
-import com.google.devtools.build.skyframe.NodeEntry.DependencyState;
import com.google.devtools.build.skyframe.ParallelEvaluator.Receiver;
import java.io.PrintStream;
@@ -224,29 +223,8 @@
return;
}
for (Entry<SkyKey, SkyValue> entry : valuesToInject.entrySet()) {
- SkyKey key = entry.getKey();
- SkyValue value = entry.getValue();
- Preconditions.checkState(value != null, key);
- NodeEntry prevEntry = graph.createIfAbsent(key);
- if (prevEntry.isDirty()) {
- // There was an existing entry for this key in the graph.
- // Get the node in the state where it is able to accept a value.
- Preconditions.checkState(prevEntry.getTemporaryDirectDeps().isEmpty(), key);
-
- DependencyState newState = prevEntry.addReverseDepAndCheckIfDone(null);
- Preconditions.checkState(newState == DependencyState.NEEDS_SCHEDULING, key);
-
- // Check that the previous node has no dependencies. Overwriting a value with deps with an
- // injected value (which is by definition deps-free) needs a little additional bookkeeping
- // (removing reverse deps from the dependencies), but more importantly it's something that
- // we want to avoid, because it indicates confusion of input values and derived values.
- Preconditions.checkState(prevEntry.noDepsLastBuild(),
- "existing entry for %s has deps: %s", key, prevEntry);
- }
- prevEntry.setValue(value, version);
- // The evaluate method previously invalidated all keys in valuesToInject that survived the
- // pruneInjectedValues call. Now that this key's injected value is set, it is no longer dirty.
- dirtyKeyTracker.notDirty(key);
+ ParallelEvaluator.injectValue(
+ entry.getKey(), entry.getValue(), version, graph, dirtyKeyTracker);
}
// Start with a new map to avoid bloat since clear() does not downsize the map.
valuesToInject = new HashMap<>();
diff --git a/src/main/java/com/google/devtools/build/skyframe/InMemoryNodeEntry.java b/src/main/java/com/google/devtools/build/skyframe/InMemoryNodeEntry.java
index fa8e308..f3a6a81 100644
--- a/src/main/java/com/google/devtools/build/skyframe/InMemoryNodeEntry.java
+++ b/src/main/java/com/google/devtools/build/skyframe/InMemoryNodeEntry.java
@@ -18,11 +18,11 @@
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.devtools.build.lib.util.GroupedList;
import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper;
import java.util.Collection;
-import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
@@ -82,49 +82,49 @@
protected boolean reverseDepIsSingleObject = false;
/**
- * During the invalidation we keep the reverse deps to be removed in this list instead of directly
- * removing them from {@code reverseDeps}. That is because removals from reverseDeps are O(N).
+ * When reverse deps are removed or checked for presence, we store them in this object instead of
+ * directly removing/checking them. That is because removals/checks in reverseDeps are O(N).
* Originally reverseDeps was a HashSet, but because of memory consumption we switched to a list.
*
* <p>This requires that any usage of reverseDeps (contains, add, the list of reverse deps) call
- * {@code consolidateReverseDepsRemovals} first. While this operation is not free, it can be done
- * more effectively than trying to remove each dirty reverse dependency individually (O(N) each
- * time).
+ * {@code consolidateData} first. While this operation is not free, it can be done
+ * more effectively than trying to remove/check each dirty reverse dependency individually (O(N)
+ * each time).
*/
- private List<SkyKey> reverseDepsToRemove = null;
+ private Object reverseDepsDataToConsolidate = null;
protected static final ReverseDepsUtil<InMemoryNodeEntry> REVERSE_DEPS_UTIL =
new ReverseDepsUtil<InMemoryNodeEntry>() {
- @Override
- void setReverseDepsObject(InMemoryNodeEntry container, Object object) {
- container.reverseDeps = object;
- }
+ @Override
+ void setReverseDepsObject(InMemoryNodeEntry container, Object object) {
+ container.reverseDeps = object;
+ }
- @Override
- void setSingleReverseDep(InMemoryNodeEntry container, boolean singleObject) {
- container.reverseDepIsSingleObject = singleObject;
- }
+ @Override
+ void setSingleReverseDep(InMemoryNodeEntry container, boolean singleObject) {
+ container.reverseDepIsSingleObject = singleObject;
+ }
- @Override
- void setReverseDepsToRemove(InMemoryNodeEntry container, List<SkyKey> object) {
- container.reverseDepsToRemove = object;
- }
+ @Override
+ void setDataToConsolidate(InMemoryNodeEntry container, Object dataToConsolidate) {
+ container.reverseDepsDataToConsolidate = dataToConsolidate;
+ }
- @Override
- Object getReverseDepsObject(InMemoryNodeEntry container) {
- return container.reverseDeps;
- }
+ @Override
+ Object getReverseDepsObject(InMemoryNodeEntry container) {
+ return container.reverseDeps;
+ }
- @Override
- boolean isSingleReverseDep(InMemoryNodeEntry container) {
- return container.reverseDepIsSingleObject;
- }
+ @Override
+ boolean isSingleReverseDep(InMemoryNodeEntry container) {
+ return container.reverseDepIsSingleObject;
+ }
- @Override
- List<SkyKey> getReverseDepsToRemove(InMemoryNodeEntry container) {
- return container.reverseDepsToRemove;
- }
- };
+ @Override
+ Object getDataToConsolidate(InMemoryNodeEntry container) {
+ return container.reverseDepsDataToConsolidate;
+ }
+ };
/**
* The transient state of this entry, after it has been created but before it is done. It allows
@@ -202,7 +202,7 @@
private synchronized Set<SkyKey> setStateFinishedAndReturnReverseDeps() {
// Get reverse deps that need to be signaled.
ImmutableSet<SkyKey> reverseDepsToSignal = buildingState.getReverseDepsToSignal();
- REVERSE_DEPS_UTIL.consolidateReverseDepsRemovals(this);
+ REVERSE_DEPS_UTIL.consolidateData(this);
REVERSE_DEPS_UTIL.addReverseDeps(this, reverseDepsToSignal);
this.directDeps = buildingState.getFinishedDirectDeps().compress();
@@ -247,7 +247,7 @@
public synchronized DependencyState addReverseDepAndCheckIfDone(SkyKey reverseDep) {
if (reverseDep != null) {
if (keepEdges()) {
- REVERSE_DEPS_UTIL.consolidateReverseDepsRemovals(this);
+ REVERSE_DEPS_UTIL.consolidateData(this);
REVERSE_DEPS_UTIL.maybeCheckReverseDepNotPresent(this, reverseDep);
}
if (isDone()) {
@@ -263,7 +263,20 @@
return DependencyState.DONE;
}
return buildingState.startEvaluating() ? DependencyState.NEEDS_SCHEDULING
- : DependencyState.ADDED_DEP;
+ : DependencyState.ALREADY_EVALUATING;
+ }
+
+ @Override
+ public synchronized DependencyState checkIfDoneForDirtyReverseDep(SkyKey reverseDep) {
+ Preconditions.checkNotNull(reverseDep, this);
+ Preconditions.checkState(keepEdges(), "%s %s", reverseDep, this);
+ if (!isDone()) {
+ REVERSE_DEPS_UTIL.removeReverseDep(this, reverseDep);
+ buildingState.addReverseDepToSignal(reverseDep);
+ } else {
+ REVERSE_DEPS_UTIL.checkReverseDep(this, reverseDep);
+ }
+ return addReverseDepAndCheckIfDone(null);
}
@Override
@@ -271,19 +284,23 @@
if (!keepEdges()) {
return;
}
- if (REVERSE_DEPS_UTIL.reverseDepsIsEmpty(this)) {
- // If an entry has no existing reverse deps, all its reverse deps are to signal, and vice
- // versa.
- buildingState.removeReverseDepToSignal(reverseDep);
- } else {
- REVERSE_DEPS_UTIL.removeReverseDep(this, reverseDep);
- }
+ REVERSE_DEPS_UTIL.removeReverseDep(this, reverseDep);
}
@Override
- public synchronized Collection<SkyKey> getReverseDeps() {
+ public synchronized void removeInProgressReverseDep(SkyKey reverseDep) {
+ buildingState.removeReverseDepToSignal(reverseDep);
+ }
+
+ @Override
+ public synchronized Iterable<SkyKey> getReverseDeps() {
assertKeepEdges();
- return REVERSE_DEPS_UTIL.getReverseDeps(this);
+ Iterable<SkyKey> reverseDeps = REVERSE_DEPS_UTIL.getReverseDeps(this);
+ if (isDone()) {
+ return reverseDeps;
+ } else {
+ return Iterables.concat(reverseDeps, buildingState.getReverseDepsToSignal());
+ }
}
@Override
@@ -313,15 +330,13 @@
}
@Override
- @Nullable
- public synchronized Iterable<SkyKey> markDirty(boolean isChanged) {
+ public synchronized boolean markDirty(boolean isChanged) {
assertKeepEdges();
if (isDone()) {
- GroupedList<SkyKey> lastDirectDeps = GroupedList.create(directDeps);
- buildingState = BuildingState.newDirtyState(isChanged, lastDirectDeps, value);
+ buildingState =
+ BuildingState.newDirtyState(isChanged, GroupedList.<SkyKey>create(directDeps), value);
value = null;
- directDeps = null;
- return lastDirectDeps.toSet();
+ return true;
}
// The caller may be simultaneously trying to mark this node dirty and changed, and the dirty
// thread may have lost the race, but it is the caller's responsibility not to try to mark
@@ -330,13 +345,12 @@
Preconditions.checkState(isChanged != isChanged(),
"Cannot mark node dirty twice or changed twice: %s", this);
Preconditions.checkState(value == null, "Value should have been reset already %s", this);
- Preconditions.checkState(directDeps == null, "direct deps not already reset %s", this);
if (isChanged) {
// If the changed marker lost the race, we just need to mark changed in this method -- all
// other work was done by the dirty marker.
buildingState.markChanged();
}
- return null;
+ return false;
}
@Override
@@ -371,10 +385,27 @@
/** @see BuildingState#getNextDirtyDirectDeps() */
@Override
public synchronized Collection<SkyKey> getNextDirtyDirectDeps() {
+ Preconditions.checkState(isReady(), this);
return buildingState.getNextDirtyDirectDeps();
}
@Override
+ public synchronized Iterable<SkyKey> getAllDirectDepsForIncompleteNode() {
+ Preconditions.checkState(!isDone(), this);
+ if (!isDirty()) {
+ return getTemporaryDirectDeps();
+ } else {
+ return Iterables.concat(
+ getTemporaryDirectDeps(), buildingState.getAllRemainingDirtyDirectDeps());
+ }
+ }
+
+ @Override
+ public synchronized Collection<SkyKey> markRebuildingAndGetAllRemainingDirtyDirectDeps() {
+ return buildingState.markRebuildingAndGetAllRemainingDirtyDirectDeps();
+ }
+
+ @Override
public synchronized Set<SkyKey> getTemporaryDirectDeps() {
Preconditions.checkState(!isDone(), "temporary shouldn't be done: %s", this);
return buildingState.getDirectDepsForBuild();
@@ -403,13 +434,15 @@
}
@Override
- public String toString() {
+ public synchronized String toString() {
return MoreObjects.toStringHelper(this)
+ .add("identity", System.identityHashCode(this))
.add("value", value)
.add("version", version)
.add("directDeps", directDeps == null ? null : GroupedList.create(directDeps))
.add("reverseDeps", REVERSE_DEPS_UTIL.toString(this))
- .add("buildingState", buildingState).toString();
+ .add("buildingState", buildingState)
+ .toString();
}
/**
diff --git a/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java b/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java
index 791155a..659f8f6 100644
--- a/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java
+++ b/src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java
@@ -239,17 +239,39 @@
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.getTemporaryDirectDeps();
- // Unregister this node from direct deps, since reverse dep edges cannot point to
- // non-existent nodes.
- for (SkyKey directDep : directDeps) {
- NodeEntry dep = graph.get(directDep);
+ 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) {
- dep.removeReverseDep(key);
+ 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.
@@ -343,40 +365,20 @@
return;
}
- // This entry remains in the graph in this dirty state until it is re-evaluated.
- Iterable<SkyKey> deps = 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 (deps == null) {
+ // 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).
+ // 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.
- Map<SkyKey, ? extends ThinNodeEntry> children = graph.getBatch(deps);
- if (children.size() != Iterables.size(deps)) {
- Set<SkyKey> depsSet = ImmutableSet.copyOf(deps);
- throw new IllegalStateException(
- "Mismatch in getBatch: "
- + key
- + ", "
- + entry
- + "\n"
- + Sets.difference(depsSet, children.keySet())
- + "\n"
- + Sets.difference(children.keySet(), depsSet));
- }
- for (ThinNodeEntry child : children.values()) {
- child.removeReverseDep(key);
- }
-
informInvalidationReceiver(key, EvaluationProgressReceiver.InvalidationState.DIRTY);
dirtyKeyTracker.dirty(key);
// Remove the node from the set as the last operation.
diff --git a/src/main/java/com/google/devtools/build/skyframe/NodeEntry.java b/src/main/java/com/google/devtools/build/skyframe/NodeEntry.java
index 56f8997..9f45ad4 100644
--- a/src/main/java/com/google/devtools/build/skyframe/NodeEntry.java
+++ b/src/main/java/com/google/devtools/build/skyframe/NodeEntry.java
@@ -29,15 +29,17 @@
*/
public interface NodeEntry extends ThinNodeEntry {
/**
- * Return code for {@link #addReverseDepAndCheckIfDone(SkyKey)}.
+ * Return code for {@link #addReverseDepAndCheckIfDone} and
+ * {@link #checkIfDoneForDirtyReverseDep}.
*/
enum DependencyState {
/** The node is done. */
DONE,
/**
- * The node was just created and needs to be scheduled for its first evaluation pass. The
- * evaluator is responsible for signaling the reverse dependency node.
+ * The node has not started evaluating, and needs to be scheduled for its first evaluation pass.
+ * The caller getting this return value is responsible for scheduling its evaluation and
+ * signaling the reverse dependency node when this node is done.
*/
NEEDS_SCHEDULING,
@@ -45,7 +47,7 @@
* The node was already created, but isn't done yet. The evaluator is responsible for
* signaling the reverse dependency node.
*/
- ADDED_DEP;
+ ALREADY_EVALUATING;
}
/**
@@ -63,9 +65,11 @@
*/
VERIFIED_CLEAN,
/**
- * A rebuilding is required or in progress, because either the node itself changed or one of
- * its dependencies did.
+ * A rebuilding is required, because either the node itself changed or one of its dependencies
+ * did.
*/
+ NEEDS_REBUILDING,
+ /** A rebuilding is in progress. */
REBUILDING
}
@@ -149,7 +153,17 @@
* if the node needs to be scheduled.
*/
@ThreadSafe
- DependencyState addReverseDepAndCheckIfDone(SkyKey reverseDep);
+ DependencyState addReverseDepAndCheckIfDone(@Nullable SkyKey reverseDep);
+
+ /**
+ * Similar to {@link #addReverseDepAndCheckIfDone}, except that {@param reverseDep} must already
+ * be a reverse dep of this entry. Should be used when reverseDep has been marked dirty and is
+ * checking its dependencies for changes. The caller must treat the return value just as they
+ * would the return value of {@link #addReverseDepAndCheckIfDone} by scheduling this node for
+ * evaluation if needed.
+ */
+ @ThreadSafe
+ DependencyState checkIfDoneForDirtyReverseDep(SkyKey reverseDep);
/**
* Tell this node that one of its dependencies is now done. Callers must check the return value,
@@ -169,7 +183,7 @@
* {@link #getVersion()}, then this entry records that one of its children has changed since it
* was last evaluated (namely, it was last evaluated at version {@link #getVersion()} and the
* child was last evaluated at {@code childVersion}. Thus, the next call to
- * {@link #getDirtyState()} will return {@link DirtyState#REBUILDING}.
+ * {@link #getDirtyState()} will return {@link DirtyState#NEEDS_REBUILDING}.
*/
@ThreadSafe
boolean signalDep(Version childVersion);
@@ -230,6 +244,37 @@
Collection<SkyKey> getNextDirtyDirectDeps();
/**
+ * Returns all deps of a node that has not yet finished evaluating. In other words, if a node has
+ * a reverse dep on this node, its key will be in the returned set here. If this node was freshly
+ * created, this is just any elements that were added using {@link #addTemporaryDirectDeps} (so it
+ * is the same as {@link #getTemporaryDirectDeps}). If this node is marked dirty, this includes
+ * all the elements that would have been returned by successive calls to
+ * {@link #getNextDirtyDirectDeps}.
+ *
+ * <p>This method should only be called when this node is about to be deleted after an aborted
+ * evaluation. After such an evaluation, any nodes that did not finish evaluating are deleted, as
+ * are any nodes that depend on them, which are necessarily also not done. If this node is to be
+ * deleted because of this, we must delete it as a reverse dep from other nodes. This method
+ * returns that list of other nodes. This method may not be called on done nodes, since they do
+ * not need to be deleted after aborted evaluations.
+ *
+ * <p>This method must not be called twice: the next thing done to this node after this method is
+ * called should be the removal of the node from the graph.
+ */
+ Iterable<SkyKey> getAllDirectDepsForIncompleteNode();
+
+ /**
+ * Notifies a node that it is about to be rebuilt. This method can only be called if the node
+ * {@link DirtyState#NEEDS_REBUILDING}. It returns the remaining deps of the node that had not
+ * yet been checked: all the keys that would be returned by successive calls to
+ * {@link #getNextDirtyDirectDeps}. It is the caller's responsibility to (uninterruptibly) remove
+ * the reverse deps those deps have on this node in order to keep the graph consistent. After this
+ * call, this node no longer has a dep on the nodes whose keys were returned by this call and
+ * is ready to be rebuilt (it will be in {@link DirtyState#REBUILDING}).
+ */
+ Collection<SkyKey> markRebuildingAndGetAllRemainingDirtyDirectDeps();
+
+ /**
* Returns the set of direct dependencies. This may only be called while the node is being
* evaluated, that is, before {@link #setValue} and after {@link #markDirty}.
*/
diff --git a/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
index 12cc1dd..a2df93a 100644
--- a/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
+++ b/src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java
@@ -40,6 +40,7 @@
import com.google.devtools.build.skyframe.EvaluationProgressReceiver.EvaluationState;
import com.google.devtools.build.skyframe.MemoizingEvaluator.EmittedEventState;
import com.google.devtools.build.skyframe.NodeEntry.DependencyState;
+import com.google.devtools.build.skyframe.NodeEntry.DirtyState;
import com.google.devtools.build.skyframe.Scheduler.SchedulerException;
import com.google.devtools.build.skyframe.SkyFunctionException.ReifiedSkyFunctionException;
@@ -582,6 +583,32 @@
}
/**
+ * If the entry is dirty and not already rebuilding, puts it in a state that it can rebuild, and
+ * removes it as a reverse dep from any dirty direct deps it had yet to check.
+ */
+ private void maybeMarkRebuildingAndRemoveRemainingDirtyDirectDeps(SkyKey key, NodeEntry entry) {
+ if (entry.isDirty() && entry.getDirtyState() != DirtyState.REBUILDING) {
+ Collection<SkyKey> depsToRemove = entry.markRebuildingAndGetAllRemainingDirtyDirectDeps();
+ Map<SkyKey, NodeEntry> depsToClearFrom = graph.getBatch(depsToRemove);
+ Preconditions.checkState(
+ depsToRemove.size() == depsToClearFrom.size(),
+ "%s %s: %s %s",
+ key,
+ entry,
+ depsToRemove,
+ depsToClearFrom);
+ for (NodeEntry depEntry : depsToClearFrom.values()) {
+ depEntry.removeReverseDep(key);
+ }
+ }
+ }
+
+ private enum DirtyOutcome {
+ ALREADY_PROCESSED,
+ NEEDS_EVALUATION
+ }
+
+ /**
* An action that evaluates a value.
*/
private class Evaluate implements Runnable {
@@ -594,20 +621,24 @@
this.skyKey = skyKey;
}
- private void enqueueChild(SkyKey skyKey, NodeEntry entry, SkyKey child) {
+ private void enqueueChild(SkyKey skyKey, NodeEntry entry, SkyKey child, boolean dirtyParent) {
Preconditions.checkState(!entry.isDone(), "%s %s", skyKey, entry);
NodeEntry depEntry = graph.createIfAbsent(child);
- switch (depEntry.addReverseDepAndCheckIfDone(skyKey)) {
- case DONE :
+ DependencyState dependencyState =
+ dirtyParent
+ ? depEntry.checkIfDoneForDirtyReverseDep(skyKey)
+ : depEntry.addReverseDepAndCheckIfDone(skyKey);
+ switch (dependencyState) {
+ case DONE:
if (entry.signalDep(depEntry.getVersion())) {
// This can only happen if there are no more children to be added.
visitor.enqueueEvaluation(skyKey);
}
break;
- case ADDED_DEP :
+ case ALREADY_EVALUATING:
break;
- case NEEDS_SCHEDULING :
+ case NEEDS_SCHEDULING:
visitor.enqueueEvaluation(child);
break;
}
@@ -623,97 +654,116 @@
&& !graph.get(ErrorTransienceValue.key()).getVersion().atMost(entry.getVersion());
}
+ private DirtyOutcome maybeHandleDirtyNode(NodeEntry state) {
+ if (!state.isDirty()) {
+ return DirtyOutcome.NEEDS_EVALUATION;
+ }
+ switch (state.getDirtyState()) {
+ case CHECK_DEPENDENCIES:
+ // Evaluating a dirty node for the first time, and checking its children to see if any
+ // of them have changed. Note that there must be dirty children for this to happen.
+
+ // Check the children group by group -- we don't want to evaluate a value that is no
+ // longer needed because an earlier dependency changed. For example, //foo:foo depends
+ // on target //bar:bar and is built. Then foo/BUILD is modified to remove the dependence
+ // on bar, and bar/BUILD is deleted. Reloading //bar:bar would incorrectly throw an
+ // exception. To avoid this, we must reload foo/BUILD first, at which point we will
+ // discover that it has changed, and re-evaluate target //foo:foo from scratch.
+ // On the other hand, when an action requests all of its inputs, we can safely check all
+ // of them in parallel on a subsequent build. So we allow checking an entire group in
+ // parallel here, if the node builder requested a group last build.
+ // Note: every dep returned here must either have this node re-registered for it (using
+ // checkIfDoneForDirtyReverseDep) and be registered as a direct dep of this node, or have
+ // its reverse dep on this node removed. Failing to do either one of these would result in
+ // a graph inconsistency, where the child had a reverse dep on this node, but this node
+ // had no kind of dependency on the child.
+ Collection<SkyKey> directDepsToCheck = state.getNextDirtyDirectDeps();
+
+ if (invalidatedByErrorTransience(directDepsToCheck, state)) {
+ // If this dep is the ErrorTransienceValue and the ErrorTransienceValue has been
+ // updated then we need to force a rebuild. We would like to just signal the entry as
+ // usual, but we can't, because then the ErrorTransienceValue would remain as a dep,
+ // which would be incorrect if, for instance, the value re-evaluated to a non-error.
+ state.forceRebuild();
+ graph.get(ErrorTransienceValue.key()).removeReverseDep(skyKey);
+ return DirtyOutcome.NEEDS_EVALUATION;
+ }
+ if (!keepGoing) {
+ // This check ensures that we maintain the invariant that if a node with an error is
+ // reached during a no-keep-going build, none of its currently building parents
+ // finishes building. If the child isn't done building yet, it will detect on its own
+ // that it has an error (see the VERIFIED_CLEAN case below). On the other hand, if it
+ // is done, then it is the parent's responsibility to notice that, which we do here.
+ // We check the deps for errors so that we don't continue building this node if it has
+ // a child error.
+ Map<SkyKey, NodeEntry> entriesToCheck = graph.getBatch(directDepsToCheck);
+ for (Map.Entry<SkyKey, NodeEntry> entry : entriesToCheck.entrySet()) {
+ if (entry.getValue().isDone() && entry.getValue().getErrorInfo() != null) {
+ // If any child has an error, we arbitrarily add a dep on the first one (needed
+ // for error bubbling) and throw an exception coming from it.
+ SkyKey errorKey = entry.getKey();
+ NodeEntry errorEntry = entry.getValue();
+ state.addTemporaryDirectDeps(GroupedListHelper.create(ImmutableList.of(errorKey)));
+ errorEntry.checkIfDoneForDirtyReverseDep(skyKey);
+ // Perform the necessary bookkeeping for any deps that are not being used.
+ for (Map.Entry<SkyKey, NodeEntry> depEntry : entriesToCheck.entrySet()) {
+ if (!depEntry.getKey().equals(errorKey)) {
+ depEntry.getValue().removeReverseDep(skyKey);
+ }
+ }
+ if (!visitor.preventNewEvaluations()) {
+ // An error was already thrown in the evaluator. Don't do anything here.
+ return DirtyOutcome.ALREADY_PROCESSED;
+ }
+ throw SchedulerException.ofError(errorEntry.getErrorInfo(), entry.getKey());
+ }
+ }
+ }
+ // It is safe to add these deps back to the node -- even if one of them has changed, the
+ // contract of pruning is that the node will request these deps again when it rebuilds.
+ // We must add these deps before enqueuing them, so that the node knows that it depends
+ // on them. If one of these deps is the error transience node, the check we did above
+ // in #invalidatedByErrorTransience means that the error transience node is not newer
+ // than this node, so we are going to mark it clean (since the error transience node is
+ // always the last dep).
+ state.addTemporaryDirectDeps(GroupedListHelper.create(directDepsToCheck));
+ for (SkyKey directDep : directDepsToCheck) {
+ enqueueChild(skyKey, state, directDep, /*dirtyParent=*/ true);
+ }
+ return DirtyOutcome.ALREADY_PROCESSED;
+ case VERIFIED_CLEAN:
+ // No child has a changed value. This node can be marked done and its parents signaled
+ // without any re-evaluation.
+ visitor.notifyDone(skyKey);
+ Set<SkyKey> reverseDeps = state.markClean();
+ if (progressReceiver != null) {
+ // Tell the receiver that the value was not actually changed this run.
+ progressReceiver.evaluated(skyKey, new SkyValueSupplier(state), EvaluationState.CLEAN);
+ }
+ if (!keepGoing && state.getErrorInfo() != null) {
+ if (!visitor.preventNewEvaluations()) {
+ return DirtyOutcome.ALREADY_PROCESSED;
+ }
+ throw SchedulerException.ofError(state.getErrorInfo(), skyKey);
+ }
+ signalValuesAndEnqueueIfReady(visitor, reverseDeps, state.getVersion());
+ return DirtyOutcome.ALREADY_PROCESSED;
+ case NEEDS_REBUILDING:
+ maybeMarkRebuildingAndRemoveRemainingDirtyDirectDeps(skyKey, state);
+ // Fall through to REBUILDING case.
+ case REBUILDING:
+ return DirtyOutcome.NEEDS_EVALUATION;
+ default:
+ throw new IllegalStateException("key: " + skyKey + ", entry: " + state);
+ }
+ }
+
@Override
public void run() {
NodeEntry state = Preconditions.checkNotNull(graph.get(skyKey), skyKey);
Preconditions.checkState(state.isReady(), "%s %s", skyKey, state);
-
- if (state.isDirty()) {
- switch (state.getDirtyState()) {
- case CHECK_DEPENDENCIES:
- // Evaluating a dirty node for the first time, and checking its children to see if any
- // of them have changed. Note that there must be dirty children for this to happen.
-
- // Check the children group by group -- we don't want to evaluate a value that is no
- // longer needed because an earlier dependency changed. For example, //foo:foo depends
- // on target //bar:bar and is built. Then foo/BUILD is modified to remove the dependence
- // on bar, and bar/BUILD is deleted. Reloading //bar:bar would incorrectly throw an
- // exception. To avoid this, we must reload foo/BUILD first, at which point we will
- // discover that it has changed, and re-evaluate target //foo:foo from scratch.
- // On the other hand, when an action requests all of its inputs, we can safely check all
- // of them in parallel on a subsequent build. So we allow checking an entire group in
- // parallel here, if the node builder requested a group last build.
- Collection<SkyKey> directDepsToCheck = state.getNextDirtyDirectDeps();
-
- if (invalidatedByErrorTransience(directDepsToCheck, state)) {
- // If this dep is the ErrorTransienceValue and the ErrorTransienceValue has been
- // updated then we need to force a rebuild. We would like to just signal the entry as
- // usual, but we can't, because then the ErrorTransienceValue would remain as a dep,
- // which would be incorrect if, for instance, the value re-evaluated to a non-error.
- state.forceRebuild();
- break; // Fall through to re-evaluation.
- }
- if (!keepGoing) {
- // This check ensures that we maintain the invariant that if a node with an error is
- // reached during a no-keep-going build, none of its currently building parents
- // finishes building. If the child isn't done building yet, it will detect on its own
- // that it has an error (see the VERIFIED_CLEAN case below). On the other hand, if it
- // is done, then it is the parent's responsibility to notice that, which we do here.
- // We check the deps for errors so that we don't continue building this node if it has
- // a child error.
- for (Map.Entry<SkyKey, NodeEntry> entry :
- graph.getBatch(directDepsToCheck).entrySet()) {
- if (entry.getValue().isDone() && entry.getValue().getErrorInfo() != null) {
- // If any child has an error, we arbitrarily add a dep on the first one (needed
- // for error bubbling) and throw an exception coming from it.
- if (!visitor.preventNewEvaluations()) {
- // An error was already thrown in the evaluator. Don't do anything here.
- return;
- }
- SkyKey errorKey = entry.getKey();
- NodeEntry errorEntry = entry.getValue();
- state.addTemporaryDirectDeps(
- GroupedListHelper.create(ImmutableList.of(errorKey)));
- DependencyState errorState = entry.getValue().addReverseDepAndCheckIfDone(skyKey);
- Preconditions.checkState(errorState == DependencyState.DONE, "%s %s %s %s",
- skyKey, state, errorKey, errorEntry);
- throw SchedulerException.ofError(errorEntry.getErrorInfo(), entry.getKey());
- }
- }
- }
- // It is safe to add these deps back to the node -- even if one of them has changed, the
- // contract of pruning is that the node will request these deps again when it rebuilds.
- // We must add these deps before enqueuing them, so that the node knows that it depends
- // on them. If one of these deps is the error transience node, the check we did above
- // in #invalidatedByErrorTransience means that the error transience node is not newer
- // than this node, so we are going to mark it clean (since the error transience node is
- // always the last dep).
- state.addTemporaryDirectDeps(GroupedListHelper.create(directDepsToCheck));
- for (SkyKey directDep : directDepsToCheck) {
- enqueueChild(skyKey, state, directDep);
- }
- return;
- case VERIFIED_CLEAN:
- // No child has a changed value. This node can be marked done and its parents signaled
- // without any re-evaluation.
- visitor.notifyDone(skyKey);
- Set<SkyKey> reverseDeps = state.markClean();
- if (progressReceiver != null) {
- // Tell the receiver that the value was not actually changed this run.
- progressReceiver.evaluated(skyKey, new SkyValueSupplier(state),
- EvaluationState.CLEAN);
- }
- if (!keepGoing && state.getErrorInfo() != null) {
- if (!visitor.preventNewEvaluations()) {
- return;
- }
- throw SchedulerException.ofError(state.getErrorInfo(), skyKey);
- }
- signalValuesAndEnqueueIfReady(visitor, reverseDeps, state.getVersion());
- return;
- case REBUILDING:
- // Nothing to be done if we are already rebuilding.
- }
+ if (maybeHandleDirtyNode(state) == DirtyOutcome.ALREADY_PROCESSED) {
+ return;
}
// TODO(bazel-team): Once deps are requested in a deterministic order within a group, or the
@@ -850,7 +900,7 @@
}
for (SkyKey newDirectDep : newDirectDeps) {
- enqueueChild(skyKey, state, newDirectDep);
+ enqueueChild(skyKey, state, newDirectDep, /*dirtyParent=*/ false);
}
// It is critical that there is no code below this point.
}
@@ -904,13 +954,13 @@
}
/**
- * If child is not done, removes key from child's reverse deps. Returns whether child should be
- * removed from key's entry's direct deps.
+ * If child is not done, removes {@param inProgressParent} from {@param child}'s reverse deps.
+ * Returns whether child should be removed from inProgressParent's entry's direct deps.
*/
- private boolean removeIncompleteChild(SkyKey key, SkyKey child) {
+ private boolean removeIncompleteChild(SkyKey inProgressParent, SkyKey child) {
NodeEntry childEntry = graph.get(child);
if (!isDoneForBuild(childEntry)) {
- childEntry.removeReverseDep(key);
+ childEntry.removeInProgressReverseDep(inProgressParent);
return true;
}
return false;
@@ -1021,18 +1071,13 @@
// in the graph, by the time that it is needed. Creating it on demand in a parallel context sets
// up a race condition, because there is no way to atomically create a node and set its value.
NodeEntry errorTransienceEntry = graph.createIfAbsent(ErrorTransienceValue.key());
- DependencyState triState = errorTransienceEntry.addReverseDepAndCheckIfDone(null);
- Preconditions.checkState(triState != DependencyState.ADDED_DEP,
- "%s %s", errorTransienceEntry, triState);
- if (triState != DependencyState.DONE) {
- errorTransienceEntry.setValue(new ErrorTransienceValue(), graphVersion);
- // The error transience entry is always invalidated by the RecordingDifferencer.
- // Now that the entry's value is set, it is no longer dirty.
- dirtyKeyTracker.notDirty(ErrorTransienceValue.key());
-
- Preconditions.checkState(
- errorTransienceEntry.addReverseDepAndCheckIfDone(null) != DependencyState.ADDED_DEP,
- errorTransienceEntry);
+ if (!errorTransienceEntry.isDone()) {
+ injectValue(
+ ErrorTransienceValue.key(),
+ new ErrorTransienceValue(),
+ graphVersion,
+ graph,
+ dirtyKeyTracker);
}
for (SkyKey skyKey : skyKeys) {
NodeEntry entry = graph.createIfAbsent(skyKey);
@@ -1044,7 +1089,7 @@
case DONE:
informProgressReceiverThatValueIsDone(skyKey);
break;
- case ADDED_DEP:
+ case ALREADY_EVALUATING:
break;
default:
throw new IllegalStateException(entry + " for " + skyKey + " in unknown state");
@@ -1135,7 +1180,7 @@
Map<SkyKey, ValueWithMetadata> bubbleErrorInfo = new HashMap<>();
boolean externalInterrupt = false;
while (true) {
- NodeEntry errorEntry = graph.get(errorKey);
+ NodeEntry errorEntry = Preconditions.checkNotNull(graph.get(errorKey), errorKey);
Iterable<SkyKey> reverseDeps = errorEntry.isDone()
? errorEntry.getReverseDeps()
: errorEntry.getInProgressReverseDeps();
@@ -1178,15 +1223,27 @@
bubbleParent, bubbleParentEntry, parentVersion, graphVersion);
continue;
}
- // Arbitrarily pick the first in-flight parent.
- Preconditions.checkState(visitor.isInflight(bubbleParent),
- "errorKey: %s, errorEntry: %s, bubbleParent: %s, bubbleParentEntry: %s", errorKey,
- errorEntry, bubbleParent, bubbleParentEntry);
- parent = bubbleParent;
- parentEntry = bubbleParentEntry;
+ if (visitor.isInflight(bubbleParent)
+ && bubbleParentEntry.getTemporaryDirectDeps().contains(errorKey)) {
+ // Only bubble up to parent if it's part of this build. If this node was dirtied and
+ // re-evaluated, but in a build without this parent, we may try to bubble up to that
+ // parent. Don't -- it's not part of the build.
+ // Similarly, the parent may not yet have requested this dep in its dirtiness-checking
+ // process. Don't bubble up to it in that case either.
+ parent = bubbleParent;
+ parentEntry = bubbleParentEntry;
+ break;
+ }
+ }
+ if (parent == null) {
+ Preconditions.checkState(
+ rootValues.contains(errorKey),
+ "Current key %s has to be a top-level key: %s, %s",
+ errorKey,
+ rootValues,
+ errorEntry);
break;
}
- Preconditions.checkNotNull(parent, "%s %s", errorKey, bubbleErrorInfo);
errorKey = parent;
SkyFunction factory = skyFunctions.get(parent.functionName());
if (parentEntry.isDirty()) {
@@ -1195,9 +1252,11 @@
// If this value's child was bubbled up to, it did not signal this value, and so we must
// manually make it ready to build.
parentEntry.signalDep();
- // Fall through to REBUILDING, since state is now REBUILDING.
+ // Fall through to NEEDS_REBUILDING, since state is now NEEDS_REBUILDING.
+ case NEEDS_REBUILDING:
+ maybeMarkRebuildingAndRemoveRemainingDirtyDirectDeps(parent, parentEntry);
+ // Fall through to REBUILDING.
case REBUILDING:
- // Nothing to be done.
break;
default:
throw new AssertionError(parent + " not in valid dirty state: " + parentEntry);
@@ -1392,6 +1451,7 @@
} else if (!entry.isReady()) {
removeIncompleteChildrenForCycle(key, entry, entry.getTemporaryDirectDeps());
}
+ maybeMarkRebuildingAndRemoveRemainingDirtyDirectDeps(key, entry);
Set<SkyKey> directDeps = entry.getTemporaryDirectDeps();
// Find out which children have errors. Similar logic to that in Evaluate#run().
List<ErrorInfo> errorDeps = getChildrenErrorsForCycle(directDeps);
@@ -1425,6 +1485,7 @@
// cycles, and this is the only missing one). Thus, it will not be removed below in
// removeDescendantsOfCycleValue, so it is safe here to signal that it is done.
entry.signalDep();
+ maybeMarkRebuildingAndRemoveRemainingDirtyDirectDeps(key, entry);
}
if (keepGoing) {
// Any children of this node that we haven't already visited are not worth visiting,
@@ -1560,6 +1621,7 @@
// that the entry can conceivably be ready if its cycleChild already found a different cycle
// and was built.
entry.signalDep();
+ maybeMarkRebuildingAndRemoveRemainingDirtyDirectDeps(key, entry);
}
Preconditions.checkState(entry.isReady(), "%s %s %s", key, cycleChild, entry);
Iterator<SkyKey> it = toVisit.iterator();
@@ -1645,4 +1707,39 @@
private boolean isDoneForBuild(@Nullable NodeEntry entry) {
return entry != null && entry.isDone();
}
+
+ static void injectValue(
+ SkyKey key,
+ SkyValue value,
+ Version version,
+ EvaluableGraph graph,
+ DirtyKeyTracker dirtyKeyTracker) {
+ Preconditions.checkNotNull(value, key);
+ NodeEntry prevEntry = graph.createIfAbsent(key);
+ DependencyState newState = prevEntry.addReverseDepAndCheckIfDone(null);
+ Preconditions.checkState(
+ newState != DependencyState.ALREADY_EVALUATING, "%s %s", key, prevEntry);
+ if (prevEntry.isDirty()) {
+ Preconditions.checkState(
+ newState == DependencyState.NEEDS_SCHEDULING, "%s %s", key, prevEntry);
+ // There was an existing entry for this key in the graph.
+ // Get the node in the state where it is able to accept a value.
+
+ // Check that the previous node has no dependencies. Overwriting a value with deps with an
+ // injected value (which is by definition deps-free) needs a little additional bookkeeping
+ // (removing reverse deps from the dependencies), but more importantly it's something that
+ // we want to avoid, because it indicates confusion of input values and derived values.
+ Preconditions.checkState(
+ prevEntry.noDepsLastBuild(), "existing entry for %s has deps: %s", key, prevEntry);
+ // Put the node into a "rebuilding" state and verify that there were no dirty deps remaining.
+ Preconditions.checkState(
+ prevEntry.markRebuildingAndGetAllRemainingDirtyDirectDeps().isEmpty(),
+ "%s %s",
+ key,
+ prevEntry);
+ }
+ prevEntry.setValue(value, version);
+ // Now that this key's injected value is set, it is no longer dirty.
+ dirtyKeyTracker.notDirty(key);
+ }
}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java b/src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java
index 807aa8c..ed8d724 100644
--- a/src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java
+++ b/src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java
@@ -21,10 +21,14 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import javax.annotation.Nullable;
+
/**
* A utility class that allows us to keep the reverse dependencies as an array list instead of a
* set. This is more memory-efficient. At the same time it allows us to group the removals and
@@ -46,13 +50,126 @@
abstract void setSingleReverseDep(T container, boolean singleObject);
- abstract void setReverseDepsToRemove(T container, List<SkyKey> object);
+ abstract void setDataToConsolidate(T container, Object dataToConsolidate);
abstract Object getReverseDepsObject(T container);
abstract boolean isSingleReverseDep(T container);
- abstract List<SkyKey> getReverseDepsToRemove(T container);
+ abstract Object getDataToConsolidate(T container);
+
+ private enum DataState {
+ NULL,
+ CHECK_ONLY,
+ BOTH
+ }
+
+ private static class RDepsToCheckAndRemove {
+ // reverseDepstoCheck may be null, but not toRemove, because we store a bare list if
+ // we have only toCheck.
+ @Nullable List<SkyKey> toCheck;
+ List<SkyKey> toRemove;
+
+ RDepsToCheckAndRemove(List<SkyKey> toCheck, List<SkyKey> toRemove) {
+ this.toCheck = toCheck;
+ this.toRemove = Preconditions.checkNotNull(toRemove);
+ }
+
+ static RDepsToCheckAndRemove justRemove(List<SkyKey> toRemove) {
+ return new RDepsToCheckAndRemove(null, toRemove);
+ }
+
+ static RDepsToCheckAndRemove replaceRemove(
+ RDepsToCheckAndRemove original, List<SkyKey> toRemove) {
+ return new RDepsToCheckAndRemove(original.toCheck, toRemove);
+ }
+
+ static RDepsToCheckAndRemove replaceCheck(
+ RDepsToCheckAndRemove original, List<SkyKey> toCheck) {
+ return new RDepsToCheckAndRemove(toCheck, original.toRemove);
+ }
+ }
+
+ private static DataState getDataState(Object reverseDepsToCheckAndRemove) {
+ if (reverseDepsToCheckAndRemove == null) {
+ return DataState.NULL;
+ } else if (reverseDepsToCheckAndRemove instanceof List) {
+ return DataState.CHECK_ONLY;
+ } else {
+ Preconditions.checkState(
+ reverseDepsToCheckAndRemove instanceof RDepsToCheckAndRemove,
+ reverseDepsToCheckAndRemove);
+ return DataState.BOTH;
+ }
+ }
+
+ private void setReverseDepsToRemove(T container, List<SkyKey> toRemove) {
+ Object reverseDepsToCheckAndRemove = getDataToConsolidate(container);
+ switch (getDataState(reverseDepsToCheckAndRemove)) {
+ case NULL:
+ reverseDepsToCheckAndRemove = RDepsToCheckAndRemove.justRemove(toRemove);
+ break;
+ case CHECK_ONLY:
+ reverseDepsToCheckAndRemove =
+ new RDepsToCheckAndRemove(((List<SkyKey>) reverseDepsToCheckAndRemove), toRemove);
+ break;
+ case BOTH:
+ reverseDepsToCheckAndRemove =
+ RDepsToCheckAndRemove.replaceRemove(
+ (RDepsToCheckAndRemove) reverseDepsToCheckAndRemove, toRemove);
+ break;
+ default:
+ throw new IllegalStateException(container + ", " + toRemove);
+ }
+ setDataToConsolidate(container, reverseDepsToCheckAndRemove);
+ }
+
+ private void setReverseDepsToCheck(T container, List<SkyKey> toCheck) {
+ Object reverseDepsToCheckAndRemove = getDataToConsolidate(container);
+ switch (getDataState(reverseDepsToCheckAndRemove)) {
+ case NULL:
+ case CHECK_ONLY:
+ reverseDepsToCheckAndRemove = toCheck;
+ break;
+ case BOTH:
+ reverseDepsToCheckAndRemove =
+ RDepsToCheckAndRemove.replaceCheck(
+ (RDepsToCheckAndRemove) reverseDepsToCheckAndRemove, toCheck);
+ break;
+ default:
+ throw new IllegalStateException(container + ", " + toCheck);
+ }
+ setDataToConsolidate(container, reverseDepsToCheckAndRemove);
+ }
+
+ @Nullable
+ private List<SkyKey> getReverseDepsToRemove(T container) {
+ Object reverseDepsToCheckAndRemove = getDataToConsolidate(container);
+ switch (getDataState(reverseDepsToCheckAndRemove)) {
+ case NULL:
+ case CHECK_ONLY:
+ return null;
+ case BOTH:
+ return ((RDepsToCheckAndRemove) reverseDepsToCheckAndRemove).toRemove;
+ default:
+ throw new IllegalStateException(reverseDepsToCheckAndRemove.toString());
+ }
+ }
+
+ @Nullable
+ private List<SkyKey> getReverseDepsToCheck(T container) {
+ Object reverseDepsToCheckAndRemove = getDataToConsolidate(container);
+ switch (getDataState(reverseDepsToCheckAndRemove)) {
+ case NULL:
+ return null;
+ case CHECK_ONLY:
+ return (List<SkyKey>) reverseDepsToCheckAndRemove;
+ case BOTH:
+ return ((RDepsToCheckAndRemove) reverseDepsToCheckAndRemove).toCheck;
+ default:
+ throw new IllegalStateException(reverseDepsToCheckAndRemove.toString());
+ }
+ }
/**
* We check that the reverse dependency is not already present. We only do that if reverseDeps is
@@ -60,15 +177,22 @@
*/
void maybeCheckReverseDepNotPresent(T container, SkyKey reverseDep) {
if (isSingleReverseDep(container)) {
- Preconditions.checkState(!getReverseDepsObject(container).equals(reverseDep),
- "Reverse dep %s already present", reverseDep);
+ Preconditions.checkState(
+ !getReverseDepsObject(container).equals(reverseDep),
+ "Reverse dep %s already present in %s",
+ reverseDep,
+ container);
return;
}
@SuppressWarnings("unchecked")
List<SkyKey> asList = (List<SkyKey>) getReverseDepsObject(container);
if (asList.size() < MAYBE_CHECK_THRESHOLD) {
- Preconditions.checkState(!asList.contains(reverseDep), "Reverse dep %s already present"
- + " in %s", reverseDep, asList);
+ Preconditions.checkState(
+ !asList.contains(reverseDep),
+ "Reverse dep %s already present in %s for %s",
+ reverseDep,
+ asList,
+ container);
}
}
@@ -106,9 +230,29 @@
}
}
- boolean reverseDepsIsEmpty(T container) {
- return !isSingleReverseDep(container)
- && ((List<SkyKey>) getReverseDepsObject(container)).isEmpty();
+ void checkReverseDep(T container, SkyKey reverseDep) {
+ Object reverseDepsObject = getReverseDepsObject(container);
+ if (isSingleReverseDep(container)) {
+ Preconditions.checkState(
+ reverseDepsObject.equals(reverseDep),
+ "%s %s %s",
+ reverseDep,
+ reverseDepsObject,
+ container);
+ return;
+ }
+ List<SkyKey> asList = (List<SkyKey>) reverseDepsObject;
+ if (asList.size() < MAYBE_CHECK_THRESHOLD) {
+ Preconditions.checkState(
+ asList.contains(reverseDep), "%s not in %s for %s", reverseDep, asList, container);
+ } else {
+ List<SkyKey> reverseDepsToCheck = getReverseDepsToCheck(container);
+ if (reverseDepsToCheck == null) {
+ reverseDepsToCheck = new ArrayList<>();
+ setReverseDepsToCheck(container, reverseDepsToCheck);
+ }
+ reverseDepsToCheck.add(reverseDep);
+ }
}
/**
@@ -122,7 +266,7 @@
"toRemove: %s container: %s",
reverseDep,
container);
- overwriteReverseDepsList(container, ImmutableList.<SkyKey>of());
+ overwriteReverseDepsList(container, ImmutableList.<SkyKey>of());
return;
}
@SuppressWarnings("unchecked")
@@ -138,7 +282,7 @@
}
ImmutableSet<SkyKey> getReverseDeps(T container) {
- consolidateReverseDepsRemovals(container);
+ consolidateData(container);
// TODO(bazel-team): Unfortunately, we need to make a copy here right now to be on the safe side
// wrt. thread-safety. The parents of a node get modified when any of the parents is deleted,
@@ -155,19 +299,38 @@
}
}
- void consolidateReverseDepsRemovals(T container) {
- List<SkyKey> reverseDepsToRemove = getReverseDepsToRemove(container);
+ void consolidateData(T container) {
Object reverseDeps = getReverseDepsObject(container);
- if (reverseDepsToRemove == null) {
+ List<SkyKey> reverseDepsToRemove = getReverseDepsToRemove(container);
+ List<SkyKey> reverseDepsToCheck = getReverseDepsToCheck(container);
+ if (reverseDepsToRemove == null && reverseDepsToCheck == null) {
return;
}
- Preconditions.checkState(!isSingleReverseDep(container),
- "We do not use reverseDepsToRemove for single lists: %s", container);
- // Should not happen, as we only create reverseDepsToRemove in case we have at least one
- // reverse dep to remove.
- Preconditions.checkState((!((List<?>) reverseDeps).isEmpty()),
- "Could not remove %s elements from %s.\nReverse deps to remove: %s. %s",
- reverseDepsToRemove.size(), reverseDeps, reverseDepsToRemove, container);
+ Preconditions.checkState(
+ !isSingleReverseDep(container),
+ "We do not delay removals/checks for single lists: %s %s %s",
+ container,
+ reverseDepsToRemove,
+ reverseDepsToCheck);
+ @SuppressWarnings("unchecked")
+ List<SkyKey> reverseDepsAsList = (List<SkyKey>) reverseDeps;
+ // Should not happen, as we only create reverseDepsToRemove/Check in case we have at least one
+ // reverse dep to remove/check.
+ Preconditions.checkState(
+ !reverseDepsAsList.isEmpty(),
+ "Could not do delayed removal/check for %s elements from %s.\n"
+ + "Reverse deps to check: %s. Container: %s",
+ reverseDepsToRemove,
+ reverseDeps,
+ reverseDepsToCheck,
+ container);
+ if (reverseDepsToRemove == null) {
+ Set<SkyKey> reverseDepsAsSet = new HashSet<>(reverseDepsAsList);
+ Preconditions.checkState(
+ reverseDepsAsSet.containsAll(reverseDepsToCheck), "%s %s", reverseDepsToCheck, container);
+ setDataToConsolidate(container, null);
+ return;
+ }
Set<SkyKey> toRemove = Sets.newHashSet(reverseDepsToRemove);
int expectedRemovals = toRemove.size();
@@ -175,12 +338,13 @@
"A reverse dependency tried to remove itself twice: %s. %s", reverseDepsToRemove,
container);
- @SuppressWarnings("unchecked")
- List<SkyKey> reverseDepsAsList = (List<SkyKey>) reverseDeps;
+ Set<SkyKey> toCheck =
+ reverseDepsToCheck == null ? new HashSet<SkyKey>() : new HashSet<>(reverseDepsToCheck);
List<SkyKey> newReverseDeps = Lists
.newArrayListWithExpectedSize(Math.max(0, reverseDepsAsList.size() - expectedRemovals));
for (SkyKey reverseDep : reverseDepsAsList) {
+ toCheck.remove(reverseDep);
if (!toRemove.contains(reverseDep)) {
newReverseDeps.add(reverseDep);
}
@@ -188,6 +352,7 @@
Preconditions.checkState(newReverseDeps.size() == reverseDepsAsList.size() - expectedRemovals,
"Could not remove some elements from %s.\nReverse deps to remove: %s. %s", reverseDeps,
toRemove, container);
+ Preconditions.checkState(toCheck.isEmpty(), "%s %s", toCheck, container);
if (newReverseDeps.isEmpty()) {
overwriteReverseDepsList(container, ImmutableList.<SkyKey>of());
@@ -196,14 +361,14 @@
} else {
overwriteReverseDepsList(container, newReverseDeps);
}
- setReverseDepsToRemove(container, null);
+ setDataToConsolidate(container, null);
}
String toString(T container) {
return MoreObjects.toStringHelper("ReverseDeps")
.add("reverseDeps", getReverseDepsObject(container))
.add("singleReverseDep", isSingleReverseDep(container))
- .add("reverseDepsToRemove", getReverseDepsToRemove(container))
+ .add("dataToConsolidate", getDataToConsolidate(container))
.toString();
}
diff --git a/src/main/java/com/google/devtools/build/skyframe/ThinNodeEntry.java b/src/main/java/com/google/devtools/build/skyframe/ThinNodeEntry.java
index 661b521..8cd16f0 100644
--- a/src/main/java/com/google/devtools/build/skyframe/ThinNodeEntry.java
+++ b/src/main/java/com/google/devtools/build/skyframe/ThinNodeEntry.java
@@ -49,6 +49,15 @@
void removeReverseDep(SkyKey reverseDep);
/**
+ * Removes a reverse dependency.
+ *
+ * <p>May only be called if this entry is not done (i.e. {@link #isDone} is false) and
+ * {@param reverseDep} is present in {@link #getReverseDeps}
+ */
+ @ThreadSafe
+ void removeInProgressReverseDep(SkyKey reverseDep);
+
+ /**
* Returns a copy of the set of reverse dependencies. Note that this introduces a potential
* check-then-act race; {@link #removeReverseDep} may fail for a key that is returned here.
*/
@@ -80,11 +89,11 @@
* the caller will only ever want to call {@code markDirty()} a second time if a transition from a
* dirty-unchanged state to a dirty-changed state is required.
*
- * @return The direct deps of this entry, or null if the entry has already been marked
- * dirty. In the latter case, the caller should abort its handling of this node, since another
- * thread is already dealing with it.
+ * @return true if the node was previously clean, and false if it was already dirty. If it was
+ * already dirty, the caller should abort its handling of this node, since another thread is
+ * already dealing with it.
*/
@Nullable
@ThreadSafe
- Iterable<SkyKey> markDirty(boolean isChanged);
+ boolean markDirty(boolean isChanged);
}
diff --git a/src/test/java/com/google/devtools/build/skyframe/DeterministicInMemoryGraph.java b/src/test/java/com/google/devtools/build/skyframe/DeterministicInMemoryGraph.java
index 2c4e861..b8c46b6 100644
--- a/src/test/java/com/google/devtools/build/skyframe/DeterministicInMemoryGraph.java
+++ b/src/test/java/com/google/devtools/build/skyframe/DeterministicInMemoryGraph.java
@@ -13,6 +13,7 @@
// limitations under the License.
package com.google.devtools.build.skyframe;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.devtools.build.lib.util.GroupedList;
import com.google.devtools.build.lib.util.GroupedList.GroupedListHelper;
@@ -69,7 +70,7 @@
@Override
public synchronized Collection<SkyKey> getReverseDeps() {
TreeSet<SkyKey> result = new TreeSet<>(ALPHABETICAL_SKYKEY_COMPARATOR);
- result.addAll(super.getReverseDeps());
+ Iterables.addAll(result, super.getReverseDeps());
return result;
}
diff --git a/src/test/java/com/google/devtools/build/skyframe/EagerInvalidatorTest.java b/src/test/java/com/google/devtools/build/skyframe/EagerInvalidatorTest.java
index 623e088..5b5790f 100644
--- a/src/test/java/com/google/devtools/build/skyframe/EagerInvalidatorTest.java
+++ b/src/test/java/com/google/devtools/build/skyframe/EagerInvalidatorTest.java
@@ -126,6 +126,10 @@
throw new UnsupportedOperationException("Sublcasses must override");
}
+ protected boolean reverseDepsPresent() {
+ throw new UnsupportedOperationException("Subclasses must override");
+ }
+
// Convenience method for eval-ing a single value.
protected SkyValue eval(boolean keepGoing, SkyKey key) throws InterruptedException {
SkyKey[] keys = { key };
@@ -349,10 +353,20 @@
assertTrue(isInvalidated(skyKey("ab")));
assertTrue(isInvalidated(skyKey("abc")));
- // The reverse deps to ab and ab_c should have been removed.
- assertThat(graph.get(skyKey("a")).getReverseDeps()).isEmpty();
- assertThat(graph.get(skyKey("b")).getReverseDeps()).containsExactly(skyKey("bc"));
- assertThat(graph.get(skyKey("c")).getReverseDeps()).containsExactly(skyKey("bc"));
+ // The reverse deps to ab and ab_c should have been removed if reverse deps are cleared.
+ Set<SkyKey> reverseDeps = new HashSet<>();
+ if (reverseDepsPresent()) {
+ reverseDeps.add(skyKey("ab"));
+ }
+ assertThat(graph.get(skyKey("a")).getReverseDeps()).containsExactlyElementsIn(reverseDeps);
+ reverseDeps.add(skyKey("bc"));
+ assertThat(graph.get(skyKey("b")).getReverseDeps()).containsExactlyElementsIn(reverseDeps);
+ reverseDeps.clear();
+ if (reverseDepsPresent()) {
+ reverseDeps.add(skyKey("ab_c"));
+ }
+ reverseDeps.add(skyKey("bc"));
+ assertThat(graph.get(skyKey("c")).getReverseDeps()).containsExactlyElementsIn(reverseDeps);
}
@Test
@@ -612,6 +626,11 @@
return InvalidationType.DELETED;
}
+ @Override
+ protected boolean reverseDepsPresent() {
+ return false;
+ }
+
@Test
public void dirtyKeyTrackerWorksWithDeletingInvalidator() throws Exception {
setupInvalidatableGraph();
@@ -628,7 +647,7 @@
Iterable<SkyKey> diff = ImmutableList.of(skyKey("a"));
Preconditions.checkNotNull(EagerInvalidator.createDeletingVisitorIfNeeded(graph, diff,
receiver, state, true, dirtyKeyTracker)).run();
- assertThat(dirtyKeyTracker.getDirtyKeys()).containsExactly(skyKey("ab"));
+ assertThat(dirtyKeyTracker.getDirtyKeys()).isEmpty();
}
}
@@ -670,6 +689,11 @@
return InvalidationType.CHANGED;
}
+ @Override
+ protected boolean reverseDepsPresent() {
+ return true;
+ }
+
@Test
public void dirtyKeyTrackerWorksWithDirtyingInvalidator() throws Exception {
setupInvalidatableGraph();
diff --git a/src/test/java/com/google/devtools/build/skyframe/GraphConcurrencyTest.java b/src/test/java/com/google/devtools/build/skyframe/GraphConcurrencyTest.java
index cf28aa8..9ad0159 100644
--- a/src/test/java/com/google/devtools/build/skyframe/GraphConcurrencyTest.java
+++ b/src/test/java/com/google/devtools/build/skyframe/GraphConcurrencyTest.java
@@ -43,6 +43,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
/** Base class for concurrency sanity tests on {@link EvaluableGraph} implementations. */
public abstract class GraphConcurrencyTest {
@@ -85,58 +86,59 @@
final NodeEntry entry = graph.createIfAbsent(key);
// These numbers are arbitrary.
int numThreads = 50;
- int numKeys = 100;
+ int numKeys = numThreads;
// One chunk will be used to add and remove rdeps before setting the node value. The second
// chunk of work will have the node value set and the last chunk will be to add and remove
// rdeps after the value has been set.
final int chunkSize = 40;
- final int numIterations = chunkSize * 3;
+ final int numIterations = chunkSize * 2;
// This latch is used to signal that the runnables have been submitted to the executor.
- final CountDownLatch countDownLatch1 = new CountDownLatch(1);
+ final CountDownLatch waitForStart = new CountDownLatch(1);
// This latch is used to signal to the main thread that we have begun the second chunk
// for sufficiently many keys. The minimum of numThreads and numKeys is used to prevent
// thread starvation from causing a delay here.
- final CountDownLatch countDownLatch2 = new CountDownLatch(Math.min(numThreads, numKeys));
+ final CountDownLatch waitForAddedRdep = new CountDownLatch(numThreads);
// This latch is used to guarantee that we set the node's value before we enter the third
// chunk for any key.
- final CountDownLatch countDownLatch3 = new CountDownLatch(1);
+ final CountDownLatch waitForSetValue = new CountDownLatch(1);
ExecutorService pool = Executors.newFixedThreadPool(numThreads);
// Add single rdep before transition to done.
assertEquals(DependencyState.NEEDS_SCHEDULING, entry.addReverseDepAndCheckIfDone(key("rdep")));
for (int i = 0; i < numKeys; i++) {
final int j = i;
- Runnable r = new Runnable() {
- @Override
- public void run() {
- try {
- countDownLatch1.await();
- // Add and remove the rdep a bunch of times to test interleaving.
- for (int k = 1; k <= numIterations; k++) {
- if (k == chunkSize) {
- countDownLatch2.countDown();
+ Runnable r =
+ new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // Add and remove the rdep a bunch of times to test interleaving.
+ waitForStart.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ for (int k = 1; k < chunkSize; k++) {
+ assertThat(entry.addReverseDepAndCheckIfDone(key("rdep" + j)))
+ .isNotEqualTo(DependencyState.DONE);
+ entry.removeInProgressReverseDep(key("rdep" + j));
+ assertThat(entry.getInProgressReverseDeps()).doesNotContain(key("rdep" + j));
+ }
+ assertThat(entry.addReverseDepAndCheckIfDone(key("rdep" + j)))
+ .isNotEqualTo(DependencyState.DONE);
+ waitForAddedRdep.countDown();
+ waitForSetValue.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ fail("Test failed: " + e.toString());
}
- entry.addReverseDepAndCheckIfDone(key("rdep" + j));
- entry.removeReverseDep(key("rdep" + j));
- if (k == chunkSize * 2) {
- countDownLatch3.await();
+ for (int k = chunkSize; k <= numIterations; k++) {
+ entry.removeReverseDep(key("rdep" + j));
+ entry.addReverseDepAndCheckIfDone(key("rdep" + j));
+ entry.getReverseDeps();
}
}
- entry.addReverseDepAndCheckIfDone(key("rdep" + j));
- } catch (InterruptedException e) {
- fail("Test failed: " + e.toString());
- }
- }
- };
+ };
pool.execute(wrapper.wrap(r));
}
- countDownLatch1.countDown();
- try {
- countDownLatch2.await();
- } catch (InterruptedException e) {
- fail("Test failed: " + e.toString());
- }
+ waitForStart.countDown();
+ waitForAddedRdep.await(TestUtils.WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
entry.setValue(new StringValue("foo1"), startingVersion);
- countDownLatch3.countDown();
+ waitForSetValue.countDown();
entry.removeReverseDep(key("rdep"));
wrapper.waitForTasksAndMaybeThrow();
assertFalse(ExecutorUtil.interruptibleShutdown(pool));
@@ -148,6 +150,7 @@
// Mark the node as dirty again and check that the reverse deps have been preserved.
sameEntry.markDirty(true);
startEvaluation(sameEntry);
+ sameEntry.markRebuildingAndGetAllRemainingDirtyDirectDeps();
sameEntry.setValue(new StringValue("foo2"), startingVersion.next());
assertEquals(new StringValue("foo2"), graph.get(key).getValue());
assertEquals(numKeys, Iterables.size(graph.get(key).getReverseDeps()));
@@ -253,6 +256,7 @@
entry.markDirty(true);
// Make some changes, like adding a dep and rdep.
entry.addReverseDepAndCheckIfDone(key("rdep"));
+ entry.markRebuildingAndGetAllRemainingDirtyDirectDeps();
addTemporaryDirectDep(entry, key("dep"));
entry.signalDep();
// Move node from dirty back to done.
diff --git a/src/test/java/com/google/devtools/build/skyframe/InMemoryNodeEntryTest.java b/src/test/java/com/google/devtools/build/skyframe/InMemoryNodeEntryTest.java
index 1976766..b598bf8 100644
--- a/src/test/java/com/google/devtools/build/skyframe/InMemoryNodeEntryTest.java
+++ b/src/test/java/com/google/devtools/build/skyframe/InMemoryNodeEntryTest.java
@@ -98,8 +98,8 @@
SkyKey mother = key("mother");
SkyKey father = key("father");
assertEquals(DependencyState.NEEDS_SCHEDULING, entry.addReverseDepAndCheckIfDone(mother));
- assertEquals(DependencyState.ADDED_DEP, entry.addReverseDepAndCheckIfDone(null));
- assertEquals(DependencyState.ADDED_DEP, entry.addReverseDepAndCheckIfDone(father));
+ assertEquals(DependencyState.ALREADY_EVALUATING, entry.addReverseDepAndCheckIfDone(null));
+ assertEquals(DependencyState.ALREADY_EVALUATING, entry.addReverseDepAndCheckIfDone(father));
assertThat(setValue(entry, new SkyValue() {},
/*errorInfo=*/null, /*graphVersion=*/0L)).containsExactly(mother, father);
assertThat(entry.getReverseDeps()).containsExactly(mother, father);
@@ -197,6 +197,7 @@
entry.signalDep();
assertThat(entry.getTemporaryDirectDeps()).containsExactly(dep);
assertTrue(entry.isReady());
+ assertThat(entry.markRebuildingAndGetAllRemainingDirtyDirectDeps()).isEmpty();
assertThat(setValue(entry, new SkyValue() {}, /*errorInfo=*/null,
/*graphVersion=*/1L)).containsExactly(parent);
}
@@ -218,9 +219,10 @@
assertTrue(entry.isReady());
SkyKey parent = key("parent");
entry.addReverseDepAndCheckIfDone(parent);
- assertEquals(NodeEntry.DirtyState.REBUILDING, entry.getDirtyState());
+ assertEquals(NodeEntry.DirtyState.NEEDS_REBUILDING, entry.getDirtyState());
assertTrue(entry.isReady());
assertThat(entry.getTemporaryDirectDeps()).isEmpty();
+ assertThat(entry.markRebuildingAndGetAllRemainingDirtyDirectDeps()).containsExactly(dep);
assertThat(setValue(entry, new SkyValue() {}, /*errorInfo=*/null,
/*graphVersion=*/1L)).containsExactly(parent);
assertEquals(new IntVersion(1L), entry.getVersion());
@@ -409,8 +411,9 @@
assertThat(entry.getNextDirtyDirectDeps()).containsExactly(dep).inOrder();
addTemporaryDirectDep(entry, dep);
entry.signalDep(new IntVersion(1L));
- assertEquals(NodeEntry.DirtyState.REBUILDING, entry.getDirtyState());
+ assertEquals(NodeEntry.DirtyState.NEEDS_REBUILDING, entry.getDirtyState());
assertThat(entry.getTemporaryDirectDeps()).containsExactly(dep);
+ assertThat(entry.markRebuildingAndGetAllRemainingDirtyDirectDeps()).isEmpty();
setValue(entry, new IntegerValue(5), /*errorInfo=*/null, /*graphVersion=*/1L);
assertTrue(entry.isDone());
assertEquals(new IntVersion(0L), entry.getVersion());
@@ -438,11 +441,12 @@
assertThat(entry.getNextDirtyDirectDeps()).containsExactly(dep).inOrder();
addTemporaryDirectDep(entry, dep);
entry.signalDep(new IntVersion(1L));
- assertEquals(NodeEntry.DirtyState.REBUILDING, entry.getDirtyState());
+ assertEquals(NodeEntry.DirtyState.NEEDS_REBUILDING, entry.getDirtyState());
assertThat(entry.getTemporaryDirectDeps()).containsExactly(dep);
ReifiedSkyFunctionException exception = new ReifiedSkyFunctionException(
new GenericFunctionException(new SomeErrorException("oops"), Transience.PERSISTENT),
key("cause"));
+ assertThat(entry.markRebuildingAndGetAllRemainingDirtyDirectDeps()).isEmpty();
setValue(entry, new IntegerValue(5), new ErrorInfo(exception),
/*graphVersion=*/1L);
assertTrue(entry.isDone());
@@ -467,8 +471,9 @@
assertThat(entry.getNextDirtyDirectDeps()).containsExactly(dep).inOrder();
addTemporaryDirectDep(entry, dep);
entry.signalDep(new IntVersion(1L));
- assertEquals(NodeEntry.DirtyState.REBUILDING, entry.getDirtyState());
+ assertEquals(NodeEntry.DirtyState.NEEDS_REBUILDING, entry.getDirtyState());
assertThat(entry.getTemporaryDirectDeps()).containsExactly(dep);
+ assertThat(entry.markRebuildingAndGetAllRemainingDirtyDirectDeps()).isEmpty();
setValue(entry, /*value=*/null, errorInfo, /*graphVersion=*/1L);
assertTrue(entry.isDone());
assertEquals(new IntVersion(0L), entry.getVersion());
@@ -542,8 +547,9 @@
assertThat(entry.getNextDirtyDirectDeps()).containsExactly(dep).inOrder();
addTemporaryDirectDep(entry, dep);
assertTrue(entry.signalDep(new IntVersion(1L)));
- assertEquals(NodeEntry.DirtyState.REBUILDING, entry.getDirtyState());
+ assertEquals(NodeEntry.DirtyState.NEEDS_REBUILDING, entry.getDirtyState());
assertThat(entry.getTemporaryDirectDeps()).containsExactly(dep);
+ assertThat(entry.markRebuildingAndGetAllRemainingDirtyDirectDeps()).isEmpty();
addTemporaryDirectDep(entry, key("dep2"));
assertTrue(entry.signalDep(new IntVersion(1L)));
setValue(entry, new IntegerValue(5), /*errorInfo=*/null, /*graphVersion=*/1L);
@@ -586,7 +592,8 @@
entry.markDirty(/*isChanged=*/true);
SkyKey newParent = key("new parent");
entry.addReverseDepAndCheckIfDone(newParent);
- assertEquals(NodeEntry.DirtyState.REBUILDING, entry.getDirtyState());
+ assertEquals(NodeEntry.DirtyState.NEEDS_REBUILDING, entry.getDirtyState());
+ assertThat(entry.markRebuildingAndGetAllRemainingDirtyDirectDeps()).isEmpty();
assertThat(setValue(entry, new SkyValue() {}, /*errorInfo=*/null,
/*graphVersion=*/1L)).containsExactly(newParent);
}
@@ -612,6 +619,8 @@
SkyKey newChild = key("newchild");
addTemporaryDirectDep(clone2, newChild);
clone2.signalDep();
+ assertThat(clone2.markRebuildingAndGetAllRemainingDirtyDirectDeps())
+ .containsExactly(originalChild);
clone2.setValue(updatedValue, version.next());
assertThat(entry.getVersion()).isEqualTo(version);
diff --git a/src/test/java/com/google/devtools/build/skyframe/MemoizingEvaluatorTest.java b/src/test/java/com/google/devtools/build/skyframe/MemoizingEvaluatorTest.java
index ffb5bf8..348f5c0 100644
--- a/src/test/java/com/google/devtools/build/skyframe/MemoizingEvaluatorTest.java
+++ b/src/test/java/com/google/devtools/build/skyframe/MemoizingEvaluatorTest.java
@@ -840,6 +840,43 @@
assertThat(cycleInfo.getPathToCycle()).containsExactly(cycleKey2).inOrder();
}
+ @Test
+ public void cycleAndSelfEdgeWithDirtyValueInSameGroup() throws Exception {
+ setGraphForTesting(new DeterministicInMemoryGraph());
+ final SkyKey cycleKey1 = GraphTester.toSkyKey("zcycleKey1");
+ final SkyKey cycleKey2 = GraphTester.toSkyKey("acycleKey2");
+ tester.getOrCreate(cycleKey2).addDependency(cycleKey2).setComputedValue(CONCATENATE);
+ tester
+ .getOrCreate(cycleKey1)
+ .setBuilder(
+ new SkyFunction() {
+ @Nullable
+ @Override
+ public SkyValue compute(SkyKey skyKey, Environment env)
+ throws SkyFunctionException, InterruptedException {
+ Map<SkyKey, SkyValue> result =
+ env.getValues(ImmutableList.of(cycleKey1, cycleKey2));
+ Preconditions.checkState(env.valuesMissing(), result);
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String extractTag(SkyKey skyKey) {
+ return null;
+ }
+ });
+ // Evaluate twice to make sure nothing strange happens with invalidation the second time.
+ for (int i = 0; i < 2; i++) {
+ EvaluationResult<SkyValue> result = tester.eval(/*keepGoing=*/ true, cycleKey1);
+ assertEquals(null, result.get(cycleKey1));
+ ErrorInfo errorInfo = result.getError(cycleKey1);
+ CycleInfo cycleInfo = Iterables.getOnlyElement(errorInfo.getCycleInfo());
+ assertThat(cycleInfo.getCycle()).containsExactly(cycleKey1).inOrder();
+ assertThat(cycleInfo.getPathToCycle()).isEmpty();
+ }
+ }
+
/** Regression test: "crash in cycle checker with dirty values". */
@Test
public void cycleWithDirtyValue() throws Exception {
@@ -1587,6 +1624,70 @@
}
@Test
+ public void removeReverseDepFromRebuildingNode() throws Exception {
+ SkyKey topKey = GraphTester.skyKey("top");
+ final SkyKey midKey = GraphTester.skyKey("mid");
+ final SkyKey changedKey = GraphTester.skyKey("changed");
+ tester.getOrCreate(changedKey).setConstantValue(new StringValue("first"));
+ // When top depends on mid,
+ tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE);
+ // And mid depends on changed,
+ tester.getOrCreate(midKey).addDependency(changedKey).setComputedValue(CONCATENATE);
+ final CountDownLatch changedKeyStarted = new CountDownLatch(1);
+ final CountDownLatch changedKeyCanFinish = new CountDownLatch(1);
+ final AtomicBoolean controlTiming = new AtomicBoolean(false);
+ setGraphForTesting(
+ new NotifyingInMemoryGraph(
+ new Listener() {
+ @Override
+ public void accept(SkyKey key, EventType type, Order order, Object context) {
+ if (!controlTiming.get()) {
+ return;
+ }
+ if (key.equals(midKey)
+ && type == EventType.CHECK_IF_DONE
+ && order == Order.BEFORE) {
+ TrackingAwaiter.INSTANCE.awaitLatchAndTrackExceptions(
+ changedKeyStarted, "changed key didn't start");
+ } else if (key.equals(changedKey)
+ && type == EventType.REMOVE_REVERSE_DEP
+ && order == Order.AFTER
+ && midKey.equals(context)) {
+ changedKeyCanFinish.countDown();
+ }
+ }
+ }));
+ // Then top builds as expected.
+ assertThat(tester.evalAndGet(/*keepGoing=*/ false, topKey)).isEqualTo(new StringValue("first"));
+ // When changed is modified,
+ tester
+ .getOrCreate(changedKey, /*markAsModified=*/ true)
+ .setConstantValue(null)
+ .setBuilder(
+ // And changed is not allowed to finish building until it is released,
+ new ChainedFunction(
+ changedKeyStarted,
+ changedKeyCanFinish,
+ null,
+ false,
+ new StringValue("second"),
+ ImmutableList.<SkyKey>of()));
+ // And mid is independently marked as modified,
+ tester.getOrCreate(midKey, /*markAsModified=*/ true);
+ tester.invalidate();
+ SkyKey newTopKey = GraphTester.skyKey("newTop");
+ // And changed will start rebuilding independently of midKey, because it's requested directly by
+ // newTop
+ tester.getOrCreate(newTopKey).addDependency(changedKey).setComputedValue(CONCATENATE);
+ // And we control the timing using the graph listener above to make sure that:
+ // (1) before we do anything with mid, changed has already started, and
+ // (2) changed key can't finish until mid tries to remove its reverse dep from changed,
+ controlTiming.set(true);
+ // Then this evaluation completes without crashing.
+ tester.eval(/*keepGoing=*/ false, newTopKey, topKey);
+ }
+
+ @Test
public void dirtyThenDeleted() throws Exception {
initializeTester();
SkyKey topKey = GraphTester.skyKey("top");
@@ -1616,15 +1717,24 @@
// leaf4 should not built in the second build.
final SkyKey leaf4 = GraphTester.toSkyKey("leaf4");
final AtomicBoolean shouldNotBuildLeaf4 = new AtomicBoolean(false);
- setGraphForTesting(new NotifyingInMemoryGraph(new Listener() {
- @Override
- public void accept(SkyKey key, EventType type, Order order, Object context) {
- if (shouldNotBuildLeaf4.get() && key.equals(leaf4)) {
- throw new IllegalStateException("leaf4 should not have been considered this build: "
- + type + ", " + order + ", " + context);
- }
- }
- }));
+ setGraphForTesting(
+ new NotifyingInMemoryGraph(
+ new Listener() {
+ @Override
+ public void accept(SkyKey key, EventType type, Order order, Object context) {
+ if (shouldNotBuildLeaf4.get()
+ && key.equals(leaf4)
+ && type != EventType.REMOVE_REVERSE_DEP) {
+ throw new IllegalStateException(
+ "leaf4 should not have been considered this build: "
+ + type
+ + ", "
+ + order
+ + ", "
+ + context);
+ }
+ }
+ }));
tester.set(leaf4, new StringValue("leaf4"));
// Create leaf0, leaf1 and leaf2 values with values "leaf2", "leaf3", "leaf4" respectively.
@@ -2548,10 +2658,10 @@
tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE);
tester.getOrCreate(midKey).addDependency(badKey).setComputedValue(CONCATENATE);
tester.getOrCreate(badKey).setHasError(true);
- EvaluationResult<SkyValue> result = tester.eval(/*keepGoing=*/false, topKey, midKey);
+ EvaluationResult<SkyValue> result = tester.eval(/*keepGoing=*/ false, topKey, midKey);
assertThat(result.getError(midKey).getRootCauses()).containsExactly(badKey);
// Do it again with keepGoing. We should also see an error for the top key this time.
- result = tester.eval(/*keepGoing=*/true, topKey, midKey);
+ result = tester.eval(/*keepGoing=*/ true, topKey, midKey);
assertThat(result.getError(midKey).getRootCauses()).containsExactly(badKey);
assertThat(result.getError(topKey).getRootCauses()).containsExactly(badKey);
}
@@ -2566,13 +2676,13 @@
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(CONCATENATE);
// When the error value throws, the propagation will cause an interrupted exception in parent.
- EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/false, parentKey);
+ EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThat(result.keyNames()).isEmpty();
Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet());
assertEquals(parentKey, error.getKey());
assertThat(error.getValue().getRootCauses()).containsExactly(errorKey);
assertFalse(Thread.interrupted());
- result = tester.eval(/*keepGoing=*/true, parentKey);
+ result = tester.eval(/*keepGoing=*/ true, parentKey);
assertThat(result.errorMap()).isEmpty();
assertEquals("recovered", result.get(parentKey).getValue());
}
@@ -2624,10 +2734,10 @@
tester.getOrCreate(topKey).addDependency(midKey).setComputedValue(CONCATENATE);
tester.getOrCreate(midKey).addDependency(badKey).setComputedValue(CONCATENATE);
tester.getOrCreate(badKey).setHasError(true);
- EvaluationResult<SkyValue> result = tester.eval(/*keepGoing=*/false, topKey, midKey);
+ EvaluationResult<SkyValue> result = tester.eval(/*keepGoing=*/ false, topKey, midKey);
assertThat(result.getError(midKey).getRootCauses()).containsExactly(badKey);
waitForSecondCall.set(true);
- result = tester.eval(/*keepGoing=*/true, topKey, midKey);
+ result = tester.eval(/*keepGoing=*/ true, topKey, midKey);
assertNotNull(firstThread.get());
assertEquals(0, otherThreadWinning.getCount());
assertThat(result.getError(midKey).getRootCauses()).containsExactly(badKey);
@@ -2645,12 +2755,12 @@
.addErrorDependency(errorKey, new StringValue("recovered"))
.setComputedValue(CONCATENATE)
.addDependency("after");
- EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/false, parentKey);
+ EvaluationResult<StringValue> result = tester.eval(/*keepGoing=*/ false, parentKey);
assertThat(result.keyNames()).isEmpty();
Map.Entry<SkyKey, ErrorInfo> error = Iterables.getOnlyElement(result.errorMap().entrySet());
assertEquals(parentKey, error.getKey());
assertThat(error.getValue().getRootCauses()).containsExactly(errorKey);
- result = tester.eval(/*keepGoing=*/true, parentKey);
+ result = tester.eval(/*keepGoing=*/ true, parentKey);
assertThat(result.errorMap()).isEmpty();
assertEquals("recoveredafter", result.get(parentKey).getValue());
}
@@ -2741,7 +2851,7 @@
// When the graph is evaluated in noKeepGoing mode,
EvaluationResult<StringValue> result =
- tester.eval(/*keepGoing=*/false, errorKey, otherErrorKey);
+ tester.eval(/*keepGoing=*/ false, errorKey, otherErrorKey);
// Then the result reports that an error occurred because of errorKey,
assertTrue(result.hasError());
@@ -3398,6 +3508,68 @@
}
}
+ @Test
+ public void cleanReverseDepFromDirtyNodeNotInBuild() throws Exception {
+ final SkyKey topKey = GraphTester.skyKey("top");
+ SkyKey inactiveKey = GraphTester.skyKey("inactive");
+ final Thread mainThread = Thread.currentThread();
+ final AtomicBoolean shouldInterrupt = new AtomicBoolean(false);
+ NotifyingInMemoryGraph graph =
+ new NotifyingInMemoryGraph(
+ new Listener() {
+ @Override
+ public void accept(SkyKey key, EventType type, Order order, Object context) {
+ if (shouldInterrupt.get()
+ && key.equals(topKey)
+ && type == EventType.IS_READY
+ && order == Order.BEFORE) {
+ mainThread.interrupt();
+ shouldInterrupt.set(false);
+ try {
+ // Make sure threadpool propagates interrupt.
+ Thread.sleep(TestUtils.WAIT_TIMEOUT_MILLISECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ });
+ setGraphForTesting(graph);
+ // When top depends on inactive,
+ tester.getOrCreate(topKey).addDependency(inactiveKey).setComputedValue(COPY);
+ StringValue val = new StringValue("inactive");
+ // And inactive is constant,
+ tester.set(inactiveKey, val);
+ // Then top evaluates normally.
+ assertThat(tester.evalAndGet(/*keepGoing=*/ true, topKey)).isEqualTo(val);
+ // When evaluation will be interrupted as soon as top starts evaluating,
+ shouldInterrupt.set(true);
+ // And inactive is dirty,
+ tester.getOrCreate(inactiveKey, /*markAsModified=*/ true);
+ // And so is top,
+ tester.getOrCreate(topKey, /*markAsModified=*/ true);
+ tester.invalidate();
+ try {
+ // Then evaluation is interrupted,
+ tester.eval(/*keepGoing=*/ false, topKey);
+ fail();
+ } catch (InterruptedException e) {
+ // Expected.
+ }
+ // But inactive is still present,
+ assertThat(graph.get(inactiveKey)).isNotNull();
+ // And still dirty,
+ assertThat(graph.get(inactiveKey).isDirty()).isTrue();
+ // And re-evaluates successfully,
+ assertThat(tester.evalAndGet(/*keepGoing=*/ true, inactiveKey)).isEqualTo(val);
+ // But top is gone from the graph,
+ assertThat(graph.get(topKey)).isNull();
+ // And we can successfully invalidate and re-evaluate inactive again.
+ tester.getOrCreate(inactiveKey, /*markAsModified=*/ true);
+ tester.invalidate();
+ assertThat(tester.evalAndGet(/*keepGoing=*/ true, inactiveKey)).isEqualTo(val);
+ }
+
/**
* A graph tester that is specific to the memoizing evaluator, with some convenience methods.
*/
diff --git a/src/test/java/com/google/devtools/build/skyframe/NotifyingInMemoryGraph.java b/src/test/java/com/google/devtools/build/skyframe/NotifyingInMemoryGraph.java
index 53f6ebf..bbed5ba 100644
--- a/src/test/java/com/google/devtools/build/skyframe/NotifyingInMemoryGraph.java
+++ b/src/test/java/com/google/devtools/build/skyframe/NotifyingInMemoryGraph.java
@@ -89,13 +89,17 @@
public enum EventType {
CREATE_IF_ABSENT,
ADD_REVERSE_DEP,
+ REMOVE_REVERSE_DEP,
SIGNAL,
SET_VALUE,
MARK_DIRTY,
MARK_CLEAN,
IS_CHANGED,
GET_VALUE_WITH_METADATA,
- IS_DIRTY
+ IS_DIRTY,
+ IS_READY,
+ CHECK_IF_DONE,
+ GET_ALL_DIRECT_DEPS_FOR_INCOMPLETE_NODE
}
public enum Order {
@@ -121,6 +125,13 @@
}
@Override
+ public synchronized void removeReverseDep(SkyKey reverseDep) {
+ graphListener.accept(myKey, EventType.REMOVE_REVERSE_DEP, Order.BEFORE, reverseDep);
+ super.removeReverseDep(reverseDep);
+ graphListener.accept(myKey, EventType.REMOVE_REVERSE_DEP, Order.AFTER, reverseDep);
+ }
+
+ @Override
public boolean signalDep(Version childVersion) {
graphListener.accept(myKey, EventType.SIGNAL, Order.BEFORE, childVersion);
boolean result = super.signalDep(childVersion);
@@ -137,9 +148,9 @@
}
@Override
- public Iterable<SkyKey> markDirty(boolean isChanged) {
+ public boolean markDirty(boolean isChanged) {
graphListener.accept(myKey, EventType.MARK_DIRTY, Order.BEFORE, isChanged);
- Iterable<SkyKey> result = super.markDirty(isChanged);
+ boolean result = super.markDirty(isChanged);
graphListener.accept(myKey, EventType.MARK_DIRTY, Order.AFTER, isChanged);
return result;
}
@@ -165,9 +176,28 @@
}
@Override
+ public synchronized boolean isReady() {
+ graphListener.accept(myKey, EventType.IS_READY, Order.BEFORE, this);
+ return super.isReady();
+ }
+
+ @Override
public SkyValue getValueMaybeWithMetadata() {
graphListener.accept(myKey, EventType.GET_VALUE_WITH_METADATA, Order.BEFORE, this);
return super.getValueMaybeWithMetadata();
}
+
+ @Override
+ public synchronized DependencyState checkIfDoneForDirtyReverseDep(SkyKey reverseDep) {
+ graphListener.accept(myKey, EventType.CHECK_IF_DONE, Order.BEFORE, reverseDep);
+ return super.checkIfDoneForDirtyReverseDep(reverseDep);
+ }
+
+ @Override
+ public synchronized Iterable<SkyKey> getAllDirectDepsForIncompleteNode() {
+ graphListener.accept(
+ myKey, EventType.GET_ALL_DIRECT_DEPS_FOR_INCOMPLETE_NODE, Order.BEFORE, this);
+ return super.getAllDirectDepsForIncompleteNode();
+ }
}
}
diff --git a/src/test/java/com/google/devtools/build/skyframe/ReverseDepsUtilTest.java b/src/test/java/com/google/devtools/build/skyframe/ReverseDepsUtilTest.java
index efcbbec..526666f 100644
--- a/src/test/java/com/google/devtools/build/skyframe/ReverseDepsUtilTest.java
+++ b/src/test/java/com/google/devtools/build/skyframe/ReverseDepsUtilTest.java
@@ -48,43 +48,49 @@
this.numElements = numElements;
}
- private static final ReverseDepsUtil<Example> REVERSE_DEPS_UTIL = new ReverseDepsUtil<Example>() {
- @Override
- void setReverseDepsObject(Example container, Object object) {
- container.reverseDeps = object;
- }
+ private static final ReverseDepsUtil<Example> REVERSE_DEPS_UTIL =
+ new ReverseDepsUtil<Example>() {
+ @Override
+ void setReverseDepsObject(Example container, Object object) {
+ container.reverseDeps = object;
+ }
- @Override
- void setSingleReverseDep(Example container, boolean singleObject) {
- container.single = singleObject;
- }
+ @Override
+ void setSingleReverseDep(Example container, boolean singleObject) {
+ container.single = singleObject;
+ }
- @Override
- void setReverseDepsToRemove(Example container, List<SkyKey> object) {
- container.reverseDepsToRemove = object;
- }
+ @Override
+ void setDataToConsolidate(Example container, Object dataToConsolidate) {
+ container.dataToConsolidate = dataToConsolidate;
+ }
- @Override
- Object getReverseDepsObject(Example container) {
- return container.reverseDeps;
- }
+ @Override
+ Object getReverseDepsObject(Example container) {
+ return container.reverseDeps;
+ }
- @Override
- boolean isSingleReverseDep(Example container) {
- return container.single;
- }
+ @Override
+ boolean isSingleReverseDep(Example container) {
+ return container.single;
+ }
- @Override
- List<SkyKey> getReverseDepsToRemove(Example container) {
- return container.reverseDepsToRemove;
- }
- };
+ @Override
+ Object getDataToConsolidate(Example container) {
+ return container.dataToConsolidate;
+ }
+ };
private class Example {
Object reverseDeps = ImmutableList.of();
boolean single;
- List<SkyKey> reverseDepsToRemove;
+ Object dataToConsolidate;
+
+ @Override
+ public String toString() {
+ return "Example: " + reverseDeps + ", " + single + ", " + dataToConsolidate;
+ }
}
@Test
@@ -101,7 +107,7 @@
REVERSE_DEPS_UTIL.removeReverseDep(example, new SkyKey(NODE_TYPE, i));
}
assertThat(REVERSE_DEPS_UTIL.getReverseDeps(example)).hasSize(numElements - numRemovals);
- assertThat(example.reverseDepsToRemove).isNull();
+ assertThat(example.dataToConsolidate).isNull();
}
}
@@ -120,7 +126,7 @@
REVERSE_DEPS_UTIL.removeReverseDep(example, new SkyKey(NODE_TYPE, i));
}
assertThat(REVERSE_DEPS_UTIL.getReverseDeps(example)).hasSize(numElements - numRemovals);
- assertThat(example.reverseDepsToRemove).isNull();
+ assertThat(example.dataToConsolidate).isNull();
}
}