aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Janak Ramakrishnan <janakr@google.com>2015-09-22 17:37:10 +0000
committerGravatar Lukacs Berki <lberki@google.com>2015-09-23 10:31:45 +0000
commit5877b8b39a7d88f4501ab1736f8ac2fe65431579 (patch)
tree946f1ee782e403e9de737a63ee66f79c0e9432df /src
parent095190962d6ddf91bd8378ff7fe4e0543f74091f (diff)
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
Diffstat (limited to 'src')
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/BuildingState.java129
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java26
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/InMemoryNodeEntry.java141
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/InvalidatingNodeVisitor.java62
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/NodeEntry.java61
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/ParallelEvaluator.java335
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/ReverseDepsUtil.java213
-rw-r--r--src/main/java/com/google/devtools/build/skyframe/ThinNodeEntry.java17
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/DeterministicInMemoryGraph.java3
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/EagerInvalidatorTest.java34
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/GraphConcurrencyTest.java66
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/InMemoryNodeEntryTest.java25
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/MemoizingEvaluatorTest.java208
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/NotifyingInMemoryGraph.java36
-rw-r--r--src/test/java/com/google/devtools/build/skyframe/ReverseDepsUtilTest.java74
15 files changed, 1010 insertions, 420 deletions
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 547549e07a..41b94ad137 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 @@ public class BuildingState {
/**
* 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 @@ public class BuildingState {
// 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 setSingleReverseDep(BuildingState container, boolean singleObject) {
- container.reverseDepIsSingleObject = singleObject;
- }
-
- @Override
- void setReverseDepsToRemove(BuildingState container, List<SkyKey> object) {
- container.reverseDepsToRemove = object;
- }
-
- @Override
- Object getReverseDepsObject(BuildingState container) {
- return container.reverseDepsToSignal;
- }
-
- @Override
- boolean isSingleReverseDep(BuildingState container) {
- return container.reverseDepIsSingleObject;
- }
-
- @Override
- List<SkyKey> getReverseDepsToRemove(BuildingState container) {
- return container.reverseDepsToRemove;
- }
- };
+ @Override
+ void setReverseDepsObject(BuildingState container, Object object) {
+ container.reverseDepsToSignal = object;
+ }
+
+ @Override
+ void setSingleReverseDep(BuildingState container, boolean singleObject) {
+ container.reverseDepIsSingleObject = singleObject;
+ }
+
+ @Override
+ void setDataToConsolidate(BuildingState container, Object dataToConsolidate) {
+ container.reverseDepsDataToConsolidate = dataToConsolidate;
+ }
+
+ @Override
+ Object getReverseDepsObject(BuildingState container) {
+ return container.reverseDepsToSignal;
+ }
+
+ @Override
+ boolean isSingleReverseDep(BuildingState container) {
+ return container.reverseDepIsSingleObject;
+ }
+
+ @Override
+ Object getDataToConsolidate(BuildingState container) {
+ return container.reverseDepsDataToConsolidate;
+ }
+ };
// Below are fields that are used for dirty nodes.
@@ -180,12 +181,10 @@ public class BuildingState {
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 @@ public class BuildingState {
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 @@ public class BuildingState {
* @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 @@ public class BuildingState {
* 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 @@ public class BuildingState {
* 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 @@ public class BuildingState {
*/
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 @@ public class BuildingState {
* 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 @@ public class BuildingState {
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 @@ public class BuildingState {
* @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 ad50bea196..9e346ebcb5 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.Differencer.Diff;
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 @@ public final class InMemoryMemoizingEvaluator implements MemoizingEvaluator {
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 fa8e3082a6..f3a6a8111a 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.MoreObjects;
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 @@ public class InMemoryNodeEntry implements NodeEntry {
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 @@ public class InMemoryNodeEntry implements NodeEntry {
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 class InMemoryNodeEntry implements NodeEntry {
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 @@ public class InMemoryNodeEntry implements NodeEntry {
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 @@ public class InMemoryNodeEntry implements NodeEntry {
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 void removeInProgressReverseDep(SkyKey reverseDep) {
+ buildingState.removeReverseDepToSignal(reverseDep);
}
@Override
- public synchronized Collection<SkyKey> getReverseDeps() {
+ 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 @@ public class InMemoryNodeEntry implements NodeEntry {
}
@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 @@ public class InMemoryNodeEntry implements NodeEntry {
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 @@ public class InMemoryNodeEntry implements NodeEntry {
/** @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 @@ public class InMemoryNodeEntry implements NodeEntry {
}
@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 791155a7dd..659f8f6665 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 @@ public abstract class InvalidatingNodeVisitor<TGraph extends ThinNodeQueryableGr
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 @@ public abstract class InvalidatingNodeVisitor<TGraph extends ThinNodeQueryableGr
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 56f89978ae..9f45ad4ede 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 @@ import javax.annotation.Nullable;
*/
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 @@ public interface NodeEntry extends ThinNodeEntry {
* 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 @@ public interface NodeEntry extends ThinNodeEntry {
*/
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 @@ public interface NodeEntry extends ThinNodeEntry {
* 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 @@ public interface NodeEntry extends ThinNodeEntry {
* {@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 @@ public interface NodeEntry extends ThinNodeEntry {
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 12cc1dd0cb..a2df93a2ed 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.lib.util.GroupedList.GroupedListHelper;
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 @@ public final class ParallelEvaluator implements Evaluator {
}
/**
+ * 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 @@ public final class ParallelEvaluator implements Evaluator {
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 @@ public final class ParallelEvaluator implements Evaluator {
&& !graph.get(ErrorTransienceValue.key()).getVersion().atMost(entry.getVersion());
}
- @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;
+ 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);
}
- 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());
}
+ 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);
- }
- 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);
+ }
+ // 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;
}
- signalValuesAndEnqueueIfReady(visitor, reverseDeps, state.getVersion());
- return;
- case REBUILDING:
- // Nothing to be done if we are already rebuilding.
- }
+ 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 (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 @@ public final class ParallelEvaluator implements Evaluator {
}
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 @@ public final class ParallelEvaluator implements Evaluator {
}
/**
- * 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 @@ public final class ParallelEvaluator implements Evaluator {
// 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 @@ public final class ParallelEvaluator implements Evaluator {
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 @@ public final class ParallelEvaluator implements Evaluator {
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 @@ public final class ParallelEvaluator implements Evaluator {
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 @@ public final class ParallelEvaluator implements Evaluator {
// 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 @@ public final class ParallelEvaluator implements Evaluator {
} 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 @@ public final class ParallelEvaluator implements Evaluator {
// 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 @@ public final class ParallelEvaluator implements Evaluator {
// 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 @@ public final class ParallelEvaluator implements Evaluator {
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 807aa8c440..ed8d724af9 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.Iterables;
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 @@ public abstract class ReverseDepsUtil<T> {
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 @@ public abstract class ReverseDepsUtil<T> {
*/
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 @@ public abstract class ReverseDepsUtil<T> {
}
}
- 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 @@ public abstract class ReverseDepsUtil<T> {
"toRemove: %s container: %s",
reverseDep,
container);
- overwriteReverseDepsList(container, ImmutableList.<SkyKey>of());
+ overwriteReverseDepsList(container, ImmutableList.<SkyKey>of());
return;
}
@SuppressWarnings("unchecked")
@@ -138,7 +282,7 @@ public abstract class ReverseDepsUtil<T> {
}
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 @@ public abstract class ReverseDepsUtil<T> {
}
}
- void consolidateReverseDepsRemovals(T container) {
- List<SkyKey> reverseDepsToRemove = getReverseDepsToRemove(container);
+ void consolidateData(T container) {
Object reverseDeps = getReverseDepsObject(container);
+ List<SkyKey> reverseDepsToRemove = getReverseDepsToRemove(container);
+ List<SkyKey> reverseDepsToCheck = getReverseDepsToCheck(container);
+ if (reverseDepsToRemove == null && reverseDepsToCheck == null) {
+ return;
+ }
+ 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;
}
- 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);
Set<SkyKey> toRemove = Sets.newHashSet(reverseDepsToRemove);
int expectedRemovals = toRemove.size();
@@ -175,12 +338,13 @@ public abstract class ReverseDepsUtil<T> {
"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 @@ public abstract class ReverseDepsUtil<T> {
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 @@ public abstract class ReverseDepsUtil<T> {
} 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 661b52141d..8cd16f0695 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 @@ public interface ThinNodeEntry {
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 @@ public interface ThinNodeEntry {
* 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 2c4e861d75..b8c46b6537 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 @@ public class DeterministicInMemoryGraph extends NotifyingInMemoryGraph {
@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 623e08874c..5b5790fdee 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 @@ public class EagerInvalidatorTest {
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 @@ public class EagerInvalidatorTest {
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 @@ public class EagerInvalidatorTest {
return InvalidationType.DELETED;
}
+ @Override
+ protected boolean reverseDepsPresent() {
+ return false;
+ }
+
@Test
public void dirtyKeyTrackerWorksWithDeletingInvalidator() throws Exception {
setupInvalidatableGraph();
@@ -628,7 +647,7 @@ public class EagerInvalidatorTest {
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 @@ public class EagerInvalidatorTest {
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 cf28aa81d9..9ad015922a 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.Set;
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 @@ public abstract class GraphConcurrencyTest {
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 @@ public abstract class GraphConcurrencyTest {
// 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 @@ public abstract class GraphConcurrencyTest {
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 1976766719..b598bf8f19 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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 @@ public class InMemoryNodeEntryTest {
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 ffb5bf8b62..348f5c0d59 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 @@ public class MemoizingEvaluatorTest {
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 @@ public class MemoizingEvaluatorTest {
}
@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 @@ public class MemoizingEvaluatorTest {
// 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 @@ public class MemoizingEvaluatorTest {
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 @@ public class MemoizingEvaluatorTest {
.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 @@ public class MemoizingEvaluatorTest {
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 @@ public class MemoizingEvaluatorTest {
.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 @@ public class MemoizingEvaluatorTest {
// 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 @@ public class MemoizingEvaluatorTest {
}
}
+ @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 53f6ebffa5..bbed5ba756 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 class NotifyingInMemoryGraph extends InMemoryGraph {
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 @@ public class NotifyingInMemoryGraph extends InMemoryGraph {
}
@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 @@ public class NotifyingInMemoryGraph extends InMemoryGraph {
}
@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 @@ public class NotifyingInMemoryGraph extends InMemoryGraph {
}
@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 efcbbec28c..526666fef8 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 @@ public class ReverseDepsUtilTest {
this.numElements = numElements;
}
- 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 setReverseDepsToRemove(Example container, List<SkyKey> object) {
- container.reverseDepsToRemove = object;
- }
-
- @Override
- Object getReverseDepsObject(Example container) {
- return container.reverseDeps;
- }
-
- @Override
- boolean isSingleReverseDep(Example container) {
- return container.single;
- }
-
- @Override
- List<SkyKey> getReverseDepsToRemove(Example container) {
- return container.reverseDepsToRemove;
- }
- };
+ 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 setDataToConsolidate(Example container, Object dataToConsolidate) {
+ container.dataToConsolidate = dataToConsolidate;
+ }
+
+ @Override
+ Object getReverseDepsObject(Example container) {
+ return container.reverseDeps;
+ }
+
+ @Override
+ boolean isSingleReverseDep(Example container) {
+ return container.single;
+ }
+
+ @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 @@ public class ReverseDepsUtilTest {
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 @@ public class ReverseDepsUtilTest {
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();
}
}