Skip to content

Commit 6bdac9f

Browse files
anakanemisonhvadehra
authored andcommitted
Implement signaled dep tracking for partial reevaluation, use for genquery
This CL teaches Skyframe to keep track of which previously requested unfinished deps have completed, for parent SkyFunctions which have opted into partial reevaluation. Those SkyFunction implementations can now take advantage of that information to avoid wasted work. This CL teaches the new genquery scope traversal SkyFunction to take advantage of this new functionality. Some terminology used in this CL ("PartialReevaluationMailbox") takes inspiration from "actor models" of concurrency, such as Erlang and Akka. Those frameworks associate a "mailbox" with each "actor" (e.g. concurrent processor of work), to buffer incoming messages. In Skyframe, keys/nodes correspond to these actors, and deps signaling their parents correspond to message passing. These mailboxes live alongside SkyKeyComputeState. SkyFunctions hoping to take advantage of this signaling mechanism must store state somewhere, and if they store it in the recommended way through the environment, they must be robust in the case that state is discarded due to memory pressure. Coupling mailboxes to that state makes implementation errors less likely, because SkyFunctions won't need to implement "have dep signals, have no compute state" recovery, because that state can't be represented. Rather, their "initial evaluation" policy should apply, and is almost certainly the right thing for them to do. SkyFunctionEnvironment's implementation is inefficient for nodes opting into partial reevaluation, especially when they take advantage of dep signaling, because SkyFunctionEnvironment "batch prefetches" previously requested deps before each reevaluation. Given regular (non-partial reevaluation) SkyFunction evaluations, this is fine, because those evaluations tend to reread previously requested deps on every restart. However, SkyFunctions opting into partial reevaluation, especially those that maintain SkyKeyComputeState, and doubly especially when they take advantage of dep signaling, are highly likely to *not* need to reread every previously requested dep. Prefetching them is wasteful. In an adversarial sequence of SkyFunction reevaluations, this could result in O(|deps|^2) work: if the SkyFunction yields after each new dep request, and all previously requested deps are reread for each reevaluation. This CL takes the first step towards fixing this inefficiency. Now, the SkyFunctionEnvironment instantiated for partial reevaluations doesn't prefetch previously requested deps from the graph. Instead, it reads them only as the SkyFunction rerequests them. Unfortunately, the SkyFunctionEnvironment still needs to be aware of whether a requested dep is new, so even the specialized environment does O(|deps|) work, constructing a Set of the node's previously requested deps on each restart. A subsequent CL will complete this effort. Note that no attempt is made to reconcile the environment's getMaxTransitiveSourceVersionSoFar functionality with partial reevaluation. The only SkyFunction which depends on this method is ActionSketchFunction, which has not opted into partial reevaluation. If it did, then this could be made to work by doing a full fetch of previously requested deps first, being aware of the performance penalty of doing so. However, maxTransitiveSourceVersion will be correct when a node gets committed, because before any commit, AbstractParallelEvaluator fetches previously requested deps -- because it needs to solve this version problem, and because it needs to ensure that all previously requested deps are still done, to be robust to rewinding. PiperOrigin-RevId: 501015898 Change-Id: I43717aada069a0374a19e55574b101067e4b57db
1 parent 78d4be9 commit 6bdac9f

13 files changed

+791
-205
lines changed

src/main/java/com/google/devtools/build/lib/rules/genquery/GenQueryDirectPackageProviderFactory.java

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
// limitations under the License.
1414
package com.google.devtools.build.lib.rules.genquery;
1515

16+
import static com.google.common.base.Preconditions.checkState;
17+
1618
import com.google.common.base.Preconditions;
1719
import com.google.common.collect.HashMultimap;
1820
import com.google.common.collect.ImmutableList;
1921
import com.google.common.collect.ImmutableMap;
2022
import com.google.common.collect.Interner;
23+
import com.google.common.collect.LinkedHashMultimap;
2124
import com.google.devtools.build.lib.cmdline.Label;
2225
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
2326
import com.google.devtools.build.lib.concurrent.BlazeInterners;
@@ -37,8 +40,12 @@
3740
import com.google.devtools.build.lib.skyframe.TargetLoadingUtil.TargetAndErrorIfAny;
3841
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
3942
import com.google.devtools.build.skyframe.AbstractSkyKey;
43+
import com.google.devtools.build.skyframe.PartialReevaluationMailbox;
44+
import com.google.devtools.build.skyframe.PartialReevaluationMailbox.Causes;
45+
import com.google.devtools.build.skyframe.PartialReevaluationMailbox.Mail;
4046
import com.google.devtools.build.skyframe.SkyFunction;
4147
import com.google.devtools.build.skyframe.SkyFunction.Environment;
48+
import com.google.devtools.build.skyframe.SkyFunction.Environment.ClassToInstanceMapSkyKeyComputeState;
4249
import com.google.devtools.build.skyframe.SkyFunction.Environment.SkyKeyComputeState;
4350
import com.google.devtools.build.skyframe.SkyFunctionException;
4451
import com.google.devtools.build.skyframe.SkyFunctionException.Transience;
@@ -58,7 +65,6 @@
5865
* information.
5966
*/
6067
public class GenQueryDirectPackageProviderFactory implements GenQueryPackageProviderFactory {
61-
6268
public static final SkyFunctionName GENQUERY_SCOPE =
6369
SkyFunctionName.createHermetic("GENQUERY_SCOPE");
6470

@@ -142,19 +148,18 @@ protected BrokenQueryScopeSkyFunctionException(
142148
* <p>([0] In the future, {@code collectedPackages} might also contain packages needed to evaluate
143149
* "buildfiles" functions; see b/123795023.)
144150
*
145-
* <p>The {@code labelsToVisitNextRestart} field contains labels of targets belonging to
151+
* <p>The {@code labelsToVisitInLaterRestart} field contains labels of targets belonging to
146152
* previously unloaded packages, the "frontier" of the last Skyframe evaluation attempt's
147153
* traversal.
148154
*/
149155
private static class ScopeTraversal implements SkyKeyComputeState {
150156
private final LinkedHashMap<PackageIdentifier, Package> collectedPackages =
151157
new LinkedHashMap<>();
152158
private final LinkedHashMap<Label, Target> collectedTargets = new LinkedHashMap<>();
153-
private final LinkedHashSet<Label> labelsToVisitNextRestart = new LinkedHashSet<>();
154159

155-
private ScopeTraversal(Collection<Label> initialScope) {
156-
labelsToVisitNextRestart.addAll(initialScope);
157-
}
160+
private final LinkedHashMap<Label, SkyKey> labelsToVisitInLaterRestart = new LinkedHashMap<>();
161+
private final LinkedHashMultimap<SkyKey, Label> labelsToVisitInverse =
162+
LinkedHashMultimap.create();
158163
}
159164

160165
@Nullable
@@ -172,28 +177,74 @@ public GenQueryPackageProvider constructPackageMap(Environment env, ImmutableLis
172177
private static GenQueryPackageProvider constructPackageMapImpl(
173178
Environment env, ImmutableList<Label> scope)
174179
throws InterruptedException, BrokenQueryScopeException {
175-
ScopeTraversal traversal = env.getState(() -> new ScopeTraversal(scope));
176180

177-
LinkedHashSet<Label> labelsToVisit = new LinkedHashSet<>(traversal.labelsToVisitNextRestart);
178-
traversal.labelsToVisitNextRestart.clear();
181+
ClassToInstanceMapSkyKeyComputeState computeState =
182+
env.getState(ClassToInstanceMapSkyKeyComputeState::new);
183+
Mail mail = PartialReevaluationMailbox.from(computeState).getMail();
184+
ScopeTraversal traversal = computeState.getInstance(ScopeTraversal.class, ScopeTraversal::new);
185+
186+
LinkedHashSet<Label> labelsToVisit = null;
187+
switch (mail.kind()) {
188+
case FRESHLY_INITIALIZED:
189+
// First evaluation, or, Skyframe compute state lost due to memory pressure or errors.
190+
// Either way, start from scratch.
191+
checkState(traversal.collectedPackages.isEmpty(), "expected empty collectedPackages");
192+
checkState(traversal.collectedTargets.isEmpty(), "expected empty collectedTargets");
193+
checkState(
194+
traversal.labelsToVisitInLaterRestart.isEmpty(),
195+
"expected empty labelsToVisitInLaterRestart");
196+
checkState(traversal.labelsToVisitInverse.isEmpty(), "expected empty labelsToVisitInverse");
197+
labelsToVisit = new LinkedHashSet<>(scope);
198+
break;
199+
case CAUSES:
200+
Causes causes = mail.causes();
201+
if (causes.other()) {
202+
labelsToVisit = new LinkedHashSet<>(traversal.labelsToVisitInLaterRestart.keySet());
203+
traversal.labelsToVisitInLaterRestart.clear();
204+
traversal.labelsToVisitInverse.clear();
205+
} else {
206+
labelsToVisit = new LinkedHashSet<>();
207+
for (SkyKey signaledDep : causes.signaledDeps()) {
208+
Collection<Label> labels = traversal.labelsToVisitInverse.asMap().remove(signaledDep);
209+
// We may have been signaled by a dep whose value was observed during a previous
210+
// restart; if so, then skip it because there is no work to do for it.
211+
if (labels != null) {
212+
for (Label label : labels) {
213+
traversal.labelsToVisitInLaterRestart.remove(label);
214+
labelsToVisit.add(label);
215+
}
216+
}
217+
}
218+
}
219+
break;
220+
case EMPTY:
221+
// This reevaluation may have been triggered by a dep which completed after our previous
222+
// reevaluation started; another reevaluation gets scheduled in such a case.
223+
//
224+
// Adding that dep's key to our mailbox raced with our reading our mailbox in that previous
225+
// reevaluation. If the add won, then we consumed the key last time, and our mailbox may now
226+
// be empty. If so, then there's no work to do now, so we return.
227+
return null;
228+
}
179229

180230
// Constructing these here minimizes garbage creation. They're used in dep traversals below.
181231
var attrDepConsumer =
182232
new LabelProcessor() {
183233
LinkedHashSet<Label> nextLabelsToVisitRef = null;
184234

185-
boolean attrDepNeedsRestart = false;
235+
SkyKey keyForAttrDepNeedingRestart = null;
186236
boolean attrDepUnvisited = false;
187237
boolean hasAspects = false;
188238
HashMultimap<Attribute, Label> transitions = null;
189239

190240
@Override
191241
public void process(Target from, @Nullable Attribute attribute, Label to) {
192-
if (hasAspects
193-
&& !attrDepNeedsRestart
194-
&& traversal.labelsToVisitNextRestart.contains(to)) {
195-
attrDepNeedsRestart = true;
196-
return;
242+
if (hasAspects && keyForAttrDepNeedingRestart == null) {
243+
SkyKey skyKey = traversal.labelsToVisitInLaterRestart.get(to);
244+
if (skyKey != null) {
245+
keyForAttrDepNeedingRestart = skyKey;
246+
return;
247+
}
197248
}
198249
if (!traversal.collectedTargets.containsKey(to)) {
199250
attrDepUnvisited = true;
@@ -202,7 +253,7 @@ public void process(Target from, @Nullable Attribute attribute, Label to) {
202253
}
203254

204255
if (hasAspects
205-
&& !attrDepNeedsRestart
256+
&& keyForAttrDepNeedingRestart == null
206257
&& !attrDepUnvisited
207258
&& attribute != null
208259
&& DependencyFilter.NO_NODEP_ATTRIBUTES.test((Rule) from, attribute)) {
@@ -234,8 +285,8 @@ public void accept(Attribute aspectAttribute, Label aspectLabel) {
234285
// 1) discover that there is a problem with the label's package. If so, this throws
235286
// BrokenQueryScopeException to stop this genquery evaluation.
236287
// 2) discover that needed package information has not been computed by Skyframe. If so,
237-
// this records that label must be visited after the next Skyframe restart by adding it
238-
// to labelsToVisitNextRestart; at that time that package information will have been
288+
// this records that label must be visited in a later Skyframe restart by adding it
289+
// to labelsToVisitInLaterRestart; at that time that package information will have been
239290
// computed.
240291
// 3) use the package information already computed by Skyframe to collect the label's target
241292
// and package.
@@ -252,35 +303,37 @@ public void accept(Attribute aspectAttribute, Label aspectLabel) {
252303
// 1) if all those dependency attributes' labels' targets have been collected, then this
253304
// code will enqueue the rule's aspect dependencies' labels for visitation.
254305
// 2) otherwise, at least one of those dependency attributes' labels must have been added to
255-
// labelsToVisitNextRestart, so the rule's aspect dependencies can't be computed during
256-
// this Skyframe restart, so the rule's label also must be visited after the next
306+
// labelsToVisitInLaterRestart, so the rule's aspect dependencies can't be computed
307+
// during this Skyframe restart, so the rule's label also must be visited in a later
257308
// Skyframe restart.
258309

259310
Target target = traversal.collectedTargets.get(label);
260311
if (target == null) {
261-
TargetAndErrorIfAny targetAndErrorIfAny;
262312
try {
263-
targetAndErrorIfAny = TargetLoadingUtil.loadTarget(env, label);
313+
Object o = TargetLoadingUtil.loadTarget(env, label);
314+
if (o instanceof TargetAndErrorIfAny) {
315+
TargetAndErrorIfAny targetAndErrorIfAny = (TargetAndErrorIfAny) o;
316+
if (!targetAndErrorIfAny.isPackageLoadedSuccessfully()) {
317+
throw new BrokenQueryScopeException(
318+
"errors were encountered while computing transitive closure of the scope.");
319+
}
320+
321+
target = targetAndErrorIfAny.getTarget();
322+
traversal.collectedTargets.put(label, target);
323+
traversal.collectedPackages.put(label.getPackageIdentifier(), target.getPackage());
324+
} else {
325+
SkyKey missingKey = (SkyKey) o;
326+
traversal.labelsToVisitInLaterRestart.put(label, missingKey);
327+
traversal.labelsToVisitInverse.put(missingKey, label);
328+
continue;
329+
}
264330
} catch (NoSuchTargetException | NoSuchPackageException e) {
265331
throw new BrokenQueryScopeException(
266332
"errors were encountered while computing transitive closure of the scope.", e);
267333
}
268-
269-
if (targetAndErrorIfAny == null) {
270-
traversal.labelsToVisitNextRestart.add(label);
271-
continue;
272-
}
273-
if (!targetAndErrorIfAny.isPackageLoadedSuccessfully()) {
274-
throw new BrokenQueryScopeException(
275-
"errors were encountered while computing transitive closure of the scope.");
276-
}
277-
278-
target = targetAndErrorIfAny.getTarget();
279-
traversal.collectedTargets.put(label, target);
280-
traversal.collectedPackages.put(label.getPackageIdentifier(), target.getPackage());
281334
}
282335

283-
attrDepConsumer.attrDepNeedsRestart = false;
336+
attrDepConsumer.keyForAttrDepNeedingRestart = null;
284337
attrDepConsumer.attrDepUnvisited = false;
285338
attrDepConsumer.hasAspects = target instanceof Rule && ((Rule) target).hasAspects();
286339
attrDepConsumer.transitions = attrDepConsumer.hasAspects ? HashMultimap.create() : null;
@@ -291,8 +344,10 @@ public void accept(Attribute aspectAttribute, Label aspectLabel) {
291344
continue;
292345
}
293346

294-
if (attrDepConsumer.attrDepNeedsRestart) {
295-
traversal.labelsToVisitNextRestart.add(label);
347+
if (attrDepConsumer.keyForAttrDepNeedingRestart != null) {
348+
traversal.labelsToVisitInLaterRestart.put(
349+
label, attrDepConsumer.keyForAttrDepNeedingRestart);
350+
traversal.labelsToVisitInverse.put(attrDepConsumer.keyForAttrDepNeedingRestart, label);
296351
continue;
297352
} else if (attrDepConsumer.attrDepUnvisited) {
298353
// This schedules label to be visited a second time during this Skyframe restart. Because
@@ -319,7 +374,7 @@ public void accept(Attribute aspectAttribute, Label aspectLabel) {
319374
}
320375
labelsToVisit = nextLabelsToVisit;
321376
}
322-
if (env.valuesMissing() || !traversal.labelsToVisitNextRestart.isEmpty()) {
377+
if (env.valuesMissing() || !traversal.labelsToVisitInLaterRestart.isEmpty()) {
323378
return null;
324379
}
325380

src/main/java/com/google/devtools/build/lib/skyframe/TargetLoadingUtil.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ private TargetLoadingUtil() {}
3838
*
3939
* <p>Establishes all Skyframe dependencies needed for incremental correctness.
4040
*
41-
* <p>Returns {@code null} if {@code env.valuesMissing()}.
41+
* <p>Returns {@link TargetAndErrorIfAny} if no dep was mising; otherwise, returns the {@link
42+
* SkyKey} specifying the missing dep.
4243
*/
43-
@Nullable
44-
public static TargetAndErrorIfAny loadTarget(Environment env, Label label)
44+
public static Object loadTarget(Environment env, Label label)
4545
throws NoSuchTargetException, NoSuchPackageException, InterruptedException {
4646
if (label.getName().contains("/")) {
4747
// This target is in a subdirectory, therefore it could potentially be invalidated by
@@ -50,18 +50,19 @@ public static TargetAndErrorIfAny loadTarget(Environment env, Label label)
5050
PackageIdentifier newPkgId =
5151
PackageIdentifier.create(label.getRepository(), containingDirectory);
5252
ContainingPackageLookupValue containingPackageLookupValue;
53+
SkyKey containingPackageKey = ContainingPackageLookupValue.key(newPkgId);
5354
try {
5455
containingPackageLookupValue =
5556
(ContainingPackageLookupValue)
5657
env.getValueOrThrow(
57-
ContainingPackageLookupValue.key(newPkgId),
58+
containingPackageKey,
5859
BuildFileNotFoundException.class,
5960
InconsistentFilesystemException.class);
6061
} catch (InconsistentFilesystemException e) {
6162
throw new NoSuchTargetException(label, e.getMessage());
6263
}
6364
if (containingPackageLookupValue == null) {
64-
return null;
65+
return containingPackageKey;
6566
}
6667

6768
if (!containingPackageLookupValue.hasContainingPackage()) {
@@ -88,7 +89,7 @@ public static TargetAndErrorIfAny loadTarget(Environment env, Label label)
8889
PackageValue packageValue =
8990
(PackageValue) env.getValueOrThrow(packageKey, NoSuchPackageException.class);
9091
if (packageValue == null) {
91-
return null;
92+
return packageKey;
9293
}
9394

9495
Package pkg = packageValue.getPackage();

src/main/java/com/google/devtools/build/lib/skyframe/TransitiveBaseTraversalFunction.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ private boolean hasDepThatSatisfies(
220220
@Nullable
221221
TargetAndErrorIfAny loadTarget(Environment env, Label label)
222222
throws NoSuchTargetException, NoSuchPackageException, InterruptedException {
223-
return TargetLoadingUtil.loadTarget(env, label);
223+
Object o = TargetLoadingUtil.loadTarget(env, label);
224+
return o instanceof TargetAndErrorIfAny ? (TargetAndErrorIfAny) o : null;
224225
}
225226

226227
/**

0 commit comments

Comments
 (0)