Skip to content

Commit 636fb5e

Browse files
haxorzcopybara-github
authored andcommitted
Fix massive performance problem in BzlLoadFunction's inlining implementation.
The old code tried to avoid inlining the same bzl file more than once, but was able to do so profitably only when there were no missing Skyframe deps. Instead, when there were missing Skyframe deps we would crawl each unique path in the full load graph, and this was exacerbated by PackageFunction and BzlLoadFunction trying to do as much work as possible on missing Skyframe deps! To fix this performance bug, we partition the set of encountered bzls into 3 disjoint sets: - "loadStack": Bzls we're in the process of loading but haven't fully visited - "successfulLoads": Bzls we've fully visited and successfully loaded to a value - "unsuccessfulLoads": Bzls we've fully visited but did not successfully load to a value, due to either a Skyframe error or a missing Skyframe dep If we are about to visit a bzl that is already in "unsuccessfulLoads" we skip over it. We also have to slightly change PackageFunction's and BzlLoadFunction's aforementioned logic for handling missing Skyframe deps. Missing Skyframe deps are relatively the biggest problem in the "cold Skyframe" situation, so that situation is the worst-case and therefore the most interesting benchmark: A internal BUILD file with a modest 567 bzl files in its load graph and a "cold Skyframe" package load time of ~3.38s with inlining disabled took ~56.66s with inlining enabled before this change (~1575% worse!). After this change, it took ~7.99s (~136% worse). This is still really bad. The remaining performance problem is due to BzlLoadFunction inlining *entailing* lots of Skyframe restarts for PackageFunction. Also make a few minor comment improvements while I am here. RELNOTES: None PiperOrigin-RevId: 323084365
1 parent ed7ec3b commit 636fb5e

File tree

3 files changed

+160
-56
lines changed

3 files changed

+160
-56
lines changed

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

Lines changed: 104 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import com.google.devtools.build.skyframe.SkyValue;
6565
import com.google.devtools.build.skyframe.ValueOrException;
6666
import java.util.HashMap;
67+
import java.util.HashSet;
6768
import java.util.LinkedHashSet;
6869
import java.util.List;
6970
import java.util.Map;
@@ -213,7 +214,7 @@ public SkyValue compute(SkyKey skyKey, Environment env)
213214
* trying to resolve its top-level {@code load()} statements. Although this work proceeds in a
214215
* single thread, multiple calling contexts may evaluate .bzls in parallel. To avoid redundant
215216
* work, they share a single (global to this Skyfunction instance) cache in lieu of the regular
216-
* Skyframe cache.
217+
* Skyframe cache. Unlike the regular Skyframe cache, this cache stores only successes.
217218
*
218219
* <p>If two calling contexts race to compute the same .bzl, each one will see a different copy of
219220
* it, and only one will end up in the shared cache. This presents a hazard: Suppose A and B both
@@ -224,24 +225,35 @@ public SkyValue compute(SkyKey skyKey, Environment env)
224225
* weren't concerned about racing, A may also reevaluate previously computed items due to cache
225226
* evictions.
226227
*
227-
* <p>To solve this, we keep a second cache, {@link InliningState#visitedBzls}, that is local to
228-
* the current calling context, and which never evicts entries. This cache is always checked in
229-
* preference to the shared one; it may deviate from the shared one in some of its entries, but
230-
* the calling context won't know the difference. (If bzl inlining is only used for the loading
231-
* phase, we don't need to worry about Starlark values from different packages interacting.) The
232-
* cache is stored as part of the {@code inliningState} passed in by the caller; the caller can
233-
* obtain this object using {@link InliningState#create}.
228+
* <p>To solve this, we keep a second cache, {@link InliningState#successfulLoads}, that is local
229+
* to the current calling context, and which never evicts entries. Like the global cache discussed
230+
* above, this cache stores only successes. This cache is always checked in preference to the
231+
* shared one; it may deviate from the shared one in some of its entries, but the calling context
232+
* won't know the difference. (Since bzl inlining is only used for the loading phase, we don't
233+
* need to worry about Starlark values from different packages interacting.) The cache is stored
234+
* as part of the {@code inliningState} passed in by the caller; the caller can obtain this object
235+
* using {@link InliningState#create}.
234236
*
235-
* <p>As an aside, note that we can't avoid having a second cache by simply naively blocking
236-
* evaluation of .bzls on retrievals from the shared cache. This is because two contexts could
237-
* deadlock while trying to evaluate an illegal {@code load()} cycle from opposite ends. It would
238-
* be possible to construct a waits-for graph and perform cycle detection, or to monitor slow
239-
* threads and do detection lazily, but these do not address the cache eviction issue.
240-
* Alternatively, we could make Starlark tolerant of reloading, but that would be tantamount to
241-
* implementing full Starlark serialization.
237+
* <p>As an aside, note that we can't avoid having {@link InliningState#successfulLoads} by simply
238+
* naively blocking evaluation of .bzls on retrievals from the shared cache. This is because two
239+
* contexts could deadlock while trying to evaluate an illegal {@code load()} cycle from opposite
240+
* ends. It would be possible to construct a waits-for graph and perform cycle detection, or to
241+
* monitor slow threads and do detection lazily, but these do not address the cache eviction
242+
* issue. Alternatively, we could make Starlark tolerant of reloading, but that would be
243+
* tantamount to implementing full Starlark serialization.
242244
*
243-
* @return the requested {@code BzlLoadValue}, or null if there is a Skyframe restart or error
245+
* <p>Since our local {@link InliningState#successfulLoads} stores only successes, a separate
246+
* concern is that we don't want to unsuccessfully visit the same .bzl more than once in the same
247+
* context. (A visitation is unsuccessful if it fails due to an error or if it cannot complete
248+
* because of a missing Skyframe dep.) To address this concern we maintain a separate {@link
249+
* InliningState#unsuccessfulLoads} set, and use this set to return null instead of duplicating an
250+
* unsuccessful visitation.
251+
*
252+
* @return the requested {@code BzlLoadValue}, or null if there was a missing Skyframe dep, an
253+
* unspecified exception in a Skyframe dep request, or if this was a duplicate unsuccessful
254+
* visitation
244255
*/
256+
// TODO(brandjon): Pick one of the nouns "load" and "bzl" and use that term consistently.
245257
@Nullable
246258
BzlLoadValue computeInline(BzlLoadValue.Key key, Environment env, InliningState inliningState)
247259
throws InconsistentFilesystemException, BzlLoadFailedException, InterruptedException {
@@ -253,15 +265,17 @@ BzlLoadValue computeInline(BzlLoadValue.Key key, Environment env, InliningState
253265
}
254266

255267
/**
256-
* Retrieves or creates the requested {@link CachedBzlLoadData} object, entering it into the
257-
* local and shared caches. This is the entry point for recursive calls to the inline code path.
268+
* Retrieves or creates the requested {@link CachedBzlLoadData} object for the given bzl, entering
269+
* it into the local and shared caches. This is the entry point for recursive calls to the inline
270+
* code path.
258271
*
259272
* <p>Skyframe calls made underneath this function will be logged in the resulting {@code
260273
* CachedBzlLoadData) (or its transitive dependencies). The given Skyframe environment must not
261274
* be a {@link RecordingSkyFunctionEnvironment}, since that would imply that calls are being
262275
* logged in both the returned value and the parent value.
263276
*
264-
* <p>Returns null on Skyframe restart or error.
277+
* @return null if there was a missing Skyframe dep, an unspecified exception in a Skyframe dep
278+
* request, or if this was a duplicate unsuccessful visitation
265279
*/
266280
@Nullable
267281
private CachedBzlLoadData computeInlineCachedData(
@@ -270,28 +284,42 @@ private CachedBzlLoadData computeInlineCachedData(
270284
// Note to refactorors: No Skyframe calls may be made before the RecordingSkyFunctionEnvironment
271285
// is set up below in computeInlineForCacheMiss.
272286

273-
// Try the caches. We must try the thread-local cache before the shared one.
274-
CachedBzlLoadData cachedData = inliningState.visitedBzls.get(key);
287+
// Try the caches of successful loads. We must try the thread-local cache before the shared, for
288+
// consistency purposes (see the javadoc of #computeInline).
289+
CachedBzlLoadData cachedData = inliningState.successfulLoads.get(key);
275290
if (cachedData == null) {
276291
cachedData = cachedBzlLoadDataManager.cache.getIfPresent(key);
277292
if (cachedData != null) {
278293
// Found a cache hit from another thread's computation; register the recorded deps as if our
279294
// thread required them for the current key. Incorporate into visitedBzls any transitive
280295
// cache hits it does not already contain.
281-
cachedData.traverse(env::registerDependencies, inliningState.visitedBzls);
296+
cachedData.traverse(env::registerDependencies, inliningState.successfulLoads);
282297
}
283298
}
284299

285-
// If that didn't work, compute it ourselves and add to the caches on success.
300+
// See if we've already unsuccessfully visited the bzl.
301+
if (inliningState.unsuccessfulLoads.contains(key)) {
302+
return null;
303+
}
304+
305+
// If we're here, the bzl must have never been visited before in this calling context. Compute
306+
// it ourselves, updating the other data structures as appropriate.
286307
if (cachedData == null) {
287-
cachedData = computeInlineForCacheMiss(key, env, inliningState);
288-
if (cachedData != null) {
289-
inliningState.visitedBzls.put(key, cachedData);
290-
cachedBzlLoadDataManager.cache.put(key, cachedData);
308+
try {
309+
cachedData = computeInlineForCacheMiss(key, env, inliningState);
310+
} finally {
311+
if (cachedData != null) {
312+
inliningState.successfulLoads.put(key, cachedData);
313+
cachedBzlLoadDataManager.cache.put(key, cachedData);
314+
} else {
315+
inliningState.unsuccessfulLoads.add(key);
316+
// Either propagate an exception or fall through for null return.
317+
}
291318
}
292319
}
293320

294-
// On success, notify the parent CachedBzlLoadData of its new child.
321+
// On success (from cache hit or from scratch), notify the parent CachedBzlLoadData of its new
322+
// child.
295323
if (cachedData != null) {
296324
inliningState.childCachedDataHandler.accept(cachedData);
297325
}
@@ -429,28 +457,50 @@ private static ContainingPackageLookupValue getContainingPackageLookupValue(
429457
*/
430458
static class InliningState {
431459
/**
432-
* The set of .bzls we're currently in the process of loading. This is used for cycle detection
433-
* since we don't have the benefit of Skyframe's internal cycle detection. The set must use
434-
* insertion order for correct error reporting.
460+
* The set of bzls we're currently in the process of loading but haven't fully visited yet. This
461+
* is used for cycle detection since we don't have the benefit of Skyframe's internal cycle
462+
* detection. The set must use insertion order for correct error reporting.
463+
*
464+
* <p>This is disjoint with {@link #successfulLoads} and {@link #unsuccessfulLoads}.
465+
*
466+
* <p>This is local to current calling context. See {@link #computeInline}.
435467
*/
436468
// Keyed on the SkyKey, not the label, since label could theoretically be ambiguous, even though
437469
// in practice keys from BUILD / WORKSPACE / @builtins don't call each other. (Not sure if
438470
// WORKSPACE chunking can cause duplicate labels to appear, but we're robust regardless.)
439471
private final LinkedHashSet<BzlLoadValue.Key> loadStack;
440472

473+
/**
474+
* Cache of bzls that have been fully visited and successfully loaded to a value.
475+
*
476+
* <p>This and {@link #unsuccessfulLoads} partition the set of fully visited bzls.
477+
*
478+
* <p>This is local to current calling context. See {@link #computeInline}.
479+
*/
480+
private final Map<BzlLoadValue.Key, CachedBzlLoadData> successfulLoads;
481+
482+
/**
483+
* Set of bzls that have been fully visited, but were not successfully loaded to a value.
484+
*
485+
* <p>This and {@link #successfulLoads} partition the set of fully visited bzls, and is disjoint
486+
* with {@link #loadStack}.
487+
*
488+
* <p>This is local to current calling context. See {@link #computeInline}.
489+
*/
490+
private final HashSet<BzlLoadValue.Key> unsuccessfulLoads;
491+
441492
/** Called when a transitive {@code CachedBzlLoadData} is produced. */
442493
private final Consumer<CachedBzlLoadData> childCachedDataHandler;
443494

444-
/** Cache local to current calling context. See {@link #computeInline}. */
445-
private final Map<BzlLoadValue.Key, CachedBzlLoadData> visitedBzls;
446-
447495
private InliningState(
448496
LinkedHashSet<BzlLoadValue.Key> loadStack,
449-
Consumer<CachedBzlLoadData> childCachedDataHandler,
450-
Map<BzlLoadValue.Key, CachedBzlLoadData> visitedBzls) {
497+
Map<BzlLoadValue.Key, CachedBzlLoadData> successfulLoads,
498+
HashSet<BzlLoadValue.Key> unsuccessfulLoads,
499+
Consumer<CachedBzlLoadData> childCachedDataHandler) {
451500
this.loadStack = loadStack;
501+
this.successfulLoads = successfulLoads;
502+
this.unsuccessfulLoads = unsuccessfulLoads;
452503
this.childCachedDataHandler = childCachedDataHandler;
453-
this.visitedBzls = visitedBzls;
454504
}
455505

456506
/**
@@ -460,12 +510,15 @@ private InliningState(
460510
static InliningState create() {
461511
return new InliningState(
462512
/*loadStack=*/ new LinkedHashSet<>(),
463-
/*childCachedDataHandler=*/ x -> {}, // No parent value to mutate.
464-
/*visitedBzls=*/ new HashMap<>());
513+
/*successfulLoads=*/ new HashMap<>(),
514+
/*unsuccessfulLoads=*/ new HashSet<>(),
515+
// No parent value to mutate
516+
/*childCachedDataHandler=*/ x -> {});
465517
}
466518

467519
private InliningState createChildState(Consumer<CachedBzlLoadData> childCachedDataHandler) {
468-
return new InliningState(loadStack, childCachedDataHandler, visitedBzls);
520+
return new InliningState(
521+
loadStack, successfulLoads, unsuccessfulLoads, childCachedDataHandler);
469522
}
470523

471524
/** Records entry to a {@code load()}, throwing an exception if a cycle is detected. */
@@ -760,8 +813,10 @@ private static List<BzlLoadValue> computeBzlLoadsWithSkyframe(
760813

761814
/**
762815
* Computes the BzlLoadValue for all given keys by reusing this instance of the BzlLoadFunction,
763-
* bypassing traditional Skyframe evaluation, returning {@code null} if Skyframe deps were missing
764-
* and have been requested.
816+
* bypassing traditional Skyframe evaluation.
817+
*
818+
* @return null if there was a missing Skyframe dep, an unspecified exception in a Skyframe dep
819+
* request, or if this was a duplicate unsuccessful visitation
765820
*/
766821
@Nullable
767822
private List<BzlLoadValue> computeBzlLoadsWithInlining(
@@ -799,10 +854,13 @@ private List<BzlLoadValue> computeBzlLoadsWithInlining(
799854
continue;
800855
}
801856
if (cachedData == null) {
802-
Preconditions.checkState(env.valuesMissing(), "no starlark load value for %s", key);
803-
// We continue making inline calls even if some requested values are missing, to maximize
804-
// the number of dependent (non-inlined) SkyFunctions that are requested, thus avoiding a
805-
// quadratic number of restarts.
857+
// A null value for `cachedData` can occur when it (or its transitive loads) has a Skyframe
858+
// dep that is missing or in error. It can also occur if there's a transitive load on a bzl
859+
// that was already seen by inliningState and which returned null (note: in this case, it's
860+
// not necessarily true that there are missing Skyframe deps because this bzl could have
861+
// already been visited unsuccessfully). In both these cases, we want to continue making our
862+
// inline calls, so as to maximize the number of dependent (non-inlined) SkyFunctions that
863+
// are requested and avoid a quadratic number of restarts.
806864
valuesMissing = true;
807865
} else {
808866
bzlLoads.add(cachedData.getValue());

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -725,7 +725,6 @@ private static List<BzlLoadValue> computeBzlLoadsWithInlining(
725725
throws InterruptedException, BzlLoadFailedException, InconsistentFilesystemException {
726726
List<BzlLoadValue> bzlLoads = Lists.newArrayListWithExpectedSize(keys.size());
727727
Exception deferredException = null;
728-
boolean valuesMissing = false;
729728
// Compute BzlLoadValue for each key, sharing the same inlining state, i.e. cache of loaded
730729
// modules. This ensures that each .bzl is loaded only once, regardless of diamond dependencies
731730
// or cache eviction. (Multiple loads of the same .bzl would screw up identity equality of some
@@ -734,31 +733,31 @@ private static List<BzlLoadValue> computeBzlLoadsWithInlining(
734733
for (BzlLoadValue.Key key : keys) {
735734
SkyValue skyValue;
736735
try {
737-
// Will complete right away if it's already cached in inliningState.
736+
// Will complete right away if this key has been seen before in inliningState -- regardless
737+
// of whether it was evaluated successfully, had missing deps, or was found to be in error.
738738
skyValue = bzlLoadFunctionForInlining.computeInline(key, env, inliningState);
739739
} catch (BzlLoadFailedException | InconsistentFilesystemException e) {
740740
// For determinism's sake while inlining, preserve the first exception and continue to run
741741
// subsequently listed loads to completion/exception, loading all transitive deps anyway.
742742
deferredException = MoreObjects.firstNonNull(deferredException, e);
743743
continue;
744744
}
745-
if (skyValue == null) {
746-
checkState(env.valuesMissing(), "no starlark load value for %s", key);
747-
// We continue making inline calls even if some requested values are missing, to
748-
// maximize the number of dependent (non-inlined) SkyFunctions that are requested, thus
749-
// avoiding a quadratic number of restarts.
750-
valuesMissing = true;
751-
} else {
745+
if (skyValue != null) {
752746
bzlLoads.add((BzlLoadValue) skyValue);
753747
}
748+
// A null value for `skyValue` can occur when it (or its transitive loads) has a Skyframe dep
749+
// that is missing or in error. It can also occur if there's a transitive load on a bzl that
750+
// was already seen by inliningState and which returned null. In both these cases, we want to
751+
// continue making our inline calls, so as to maximize the number of dependent (non-inlined)
752+
// SkyFunctions that are requested and avoid a quadratic number of restarts.
754753
}
755754
if (deferredException != null) {
756755
Throwables.throwIfInstanceOf(deferredException, BzlLoadFailedException.class);
757756
Throwables.throwIfInstanceOf(deferredException, InconsistentFilesystemException.class);
758757
throw new IllegalStateException(
759758
"caught a checked exception of unexpected type", deferredException);
760759
}
761-
return valuesMissing ? null : bzlLoads;
760+
return env.valuesMissing() ? null : bzlLoads;
762761
}
763762

764763
private static int getOriginalWorkspaceChunk(

src/test/java/com/google/devtools/build/lib/skylark/StarlarkIntegrationTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3435,4 +3435,51 @@ private void setUpCustomMallocRule() throws IOException {
34353435
"",
34363436
"malloc_rule(name = 'malloc')");
34373437
}
3438+
3439+
// Test for an interesting situation for the inlining implementation's attempt to process
3440+
// subsequent load statements even when an earlier one has a missing Skyframe dep.
3441+
@Test
3442+
public void bzlFileWithErrorsLoadedThroughMultipleLoadPathsWithTheLatterOneHavingMissingDeps()
3443+
throws Exception {
3444+
scratch.file("test/starlark/error.bzl", "nope");
3445+
scratch.file("test/starlark/ok.bzl", "ok = 42");
3446+
scratch.file(
3447+
"test/starlark/loads-error-and-has-missing-deps.bzl",
3448+
"load('//test/starlark:error.bzl', 'doesntmatter')",
3449+
"load('//test/starlark:ok.bzl', 'ok')");
3450+
scratch.file(
3451+
"test/starlark/BUILD",
3452+
"load('//test/starlark:error.bzl', 'doesntmatter')",
3453+
"load('//test/starlark:loads-error-and-has-missing-deps.bzl', 'doesntmatter')");
3454+
3455+
reporter.removeHandler(failFastHandler);
3456+
BuildFileContainsErrorsException e =
3457+
assertThrows(
3458+
BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:BUILD"));
3459+
assertThat(e).hasMessageThat().contains("Extension 'test/starlark/error.bzl' has errors");
3460+
}
3461+
3462+
// Test for an interesting situation for the inlining implementation's attempt to process
3463+
// subsequent load statements even when an earlier one has a missing Skyframe dep.
3464+
@Test
3465+
public void bzlFileWithErrorsLoadedThroughMultipleLoadPathsWithTheLatterOneNotHavingMissingDeps()
3466+
throws Exception {
3467+
scratch.file("test/starlark/error.bzl", "nope");
3468+
scratch.file("test/starlark/ok.bzl", "ok = 42");
3469+
scratch.file(
3470+
"test/starlark/loads-error-and-has-missing-deps.bzl",
3471+
"load('//test/starlark:error.bzl', 'doesntmatter')",
3472+
"load('//test/starlark:ok.bzl', 'ok')");
3473+
scratch.file(
3474+
"test/starlark/BUILD",
3475+
"load('//test/starlark:ok.bzl', 'ok')",
3476+
"load('//test/starlark:error.bzl', 'doesntmatter')",
3477+
"load('//test/starlark:loads-error-and-has-missing-deps.bzl', 'doesntmatter')");
3478+
3479+
reporter.removeHandler(failFastHandler);
3480+
BuildFileContainsErrorsException e =
3481+
assertThrows(
3482+
BuildFileContainsErrorsException.class, () -> getTarget("//test/starlark:BUILD"));
3483+
assertThat(e).hasMessageThat().contains("Extension 'test/starlark/error.bzl' has errors");
3484+
}
34383485
}

0 commit comments

Comments
 (0)