From 693215317a6732085731809266f63ff0e7fc31a5 Mon Sep 17 00:00:00 2001 From: Googler Date: Tue, 6 Feb 2024 05:57:28 -0800 Subject: [PATCH] skyfocus: refactor focus command into BlazeModule (SkyfocusModule) and implement `build --experimental_working_set=`. The new `build` flags are: * `--experimental_enable_skyfocus`: the main feature flag. * `--experimental_working_set`: replacement of `focus --experimental_working_set` * `--experimental_skyfocus_dump_keys`: replacement of `focus --dump_keys` * `--experimental_skyfocus_dump_post_gc_stats`: replacement of `focus --dump_used_heap_size_after_gc` In addition, with the move to the `BlazeModule`, I also replaced the trigger point for Skyfocus with an EventBus `@Subscribe`r. The nice thing about that is we can easily hook Skyfocus into various points of the Blaze lifecycle, such as post-analysis for reducing peak heap in the future, instead of just post-execution for reducing retained heap. I do acknowledge that EventBus makes debugging and tracing the control flow a bit more difficult, but that's a tradeoff I believe it's worth making for the added lifecycle control. There is no change to the `focus` algorithm in this CL. [1] https://github.com/google/guava/wiki/EventBusExplained#avoid-eventbus PiperOrigin-RevId: 604620930 Change-Id: Ie0713f7ae0dc02babfcfdf7a89df5d85a1c35d89 --- .../com/google/devtools/build/lib/bazel/BUILD | 1 + .../devtools/build/lib/bazel/Bazel.java | 1 + .../build/lib/buildtool/BuildTool.java | 1 - .../build/lib/runtime/SkyfocusOptions.java | 67 +++ .../lib/runtime/commands/BuildCommand.java | 4 +- .../commands/BuiltinCommandModule.java | 1 - .../lib/runtime/commands/FocusCommand.java | 296 -------------- .../google/devtools/build/lib/skyframe/BUILD | 22 +- .../build/lib/skyframe/SkyfocusModule.java | 385 ++++++++++++++++++ .../skyframe/InMemoryMemoizingEvaluator.java | 41 +- .../build/skyframe/MemoizingEvaluator.java | 14 +- .../build/skyframe/SkyframeFocuser.java | 27 +- src/test/shell/integration/focus_test.sh | 95 ++++- 13 files changed, 610 insertions(+), 345 deletions(-) create mode 100644 src/main/java/com/google/devtools/build/lib/runtime/SkyfocusOptions.java delete mode 100644 src/main/java/com/google/devtools/build/lib/runtime/commands/FocusCommand.java create mode 100644 src/main/java/com/google/devtools/build/lib/skyframe/SkyfocusModule.java diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/BUILD index 709c010c9ab11c..e2844c8920ea2e 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/BUILD +++ b/src/main/java/com/google/devtools/build/lib/bazel/BUILD @@ -196,6 +196,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/remote", "//src/main/java/com/google/devtools/build/lib/runtime/mobileinstall", "//src/main/java/com/google/devtools/build/lib/sandbox:sandbox_module", + "//src/main/java/com/google/devtools/build/lib/skyframe:skyfocus_module", "//src/main/java/com/google/devtools/build/lib/skyframe:skymeld_module", "//src/main/java/com/google/devtools/build/lib/standalone", "//src/main/java/com/google/devtools/build/lib/starlarkdebug/module", diff --git a/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java b/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java index 3b54b229229701..02b3eaf1ebc474 100644 --- a/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java +++ b/src/main/java/com/google/devtools/build/lib/bazel/Bazel.java @@ -86,6 +86,7 @@ public final class Bazel { BazelBuiltinCommandModule.class, com.google.devtools.build.lib.includescanning.IncludeScanningModule.class, com.google.devtools.build.lib.skyframe.SkymeldModule.class, + com.google.devtools.build.lib.skyframe.SkyfocusModule.class, // This module needs to be registered after any module submitting tasks with its {@code // submit} method. com.google.devtools.build.lib.runtime.BlockWaitingModule.class); diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java index 8ae3fc3fdebe2d..f6dbb85fb34c51 100644 --- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java +++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildTool.java @@ -655,7 +655,6 @@ public void stopRequest( InterruptedException ie = null; try { env.getSkyframeExecutor().notifyCommandComplete(env.getReporter()); - env.getSkyframeExecutor().getEvaluator().updateTopLevelEvaluations(); } catch (InterruptedException e) { env.getReporter().handle(Event.error("Build interrupted during command completion")); ie = e; diff --git a/src/main/java/com/google/devtools/build/lib/runtime/SkyfocusOptions.java b/src/main/java/com/google/devtools/build/lib/runtime/SkyfocusOptions.java new file mode 100644 index 00000000000000..5aab07e122cb64 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/SkyfocusOptions.java @@ -0,0 +1,67 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.runtime; + +import com.google.devtools.common.options.Converters; +import com.google.devtools.common.options.Option; +import com.google.devtools.common.options.OptionDocumentationCategory; +import com.google.devtools.common.options.OptionEffectTag; +import com.google.devtools.common.options.OptionsBase; +import java.util.List; + +/** The set of options for the Skyfocus feature. */ +public final class SkyfocusOptions extends OptionsBase { + + @Option( + name = "experimental_enable_skyfocus", + defaultValue = "false", + effectTags = OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS, + documentationCategory = OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION, + help = + "If true, enable the use of --experimental_working_set to reduce Bazel's memory footprint" + + " for incremental builds. This feature is known as Skyfocus.") + public boolean skyfocusEnabled; + + @Option( + name = "experimental_working_set", + defaultValue = "", + effectTags = OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS, + documentationCategory = OptionDocumentationCategory.BUILD_TIME_OPTIMIZATION, + converter = Converters.CommaSeparatedOptionListConverter.class, + help = + "The working set for Skyfocus. Specify as comma-separated workspace root-relative paths." + + " This is a stateful flag. Defining a working set persists it for subsequent" + + " invocations, until it is redefined with a new set.") + public List workingSet; + + @Option( + name = "experimental_skyfocus_dump_keys", + defaultValue = "false", + effectTags = OptionEffectTag.TERMINAL_OUTPUT, + documentationCategory = OptionDocumentationCategory.LOGGING, + help = + "For debugging Skyfocus. Dump the focused SkyKeys (roots, leafs, focused deps, focused" + + " rdeps).") + public boolean dumpKeys; + + @Option( + name = "experimental_skyfocus_dump_post_gc_stats", + defaultValue = "false", + effectTags = OptionEffectTag.TERMINAL_OUTPUT, + documentationCategory = OptionDocumentationCategory.LOGGING, + help = + "For debugging Skyfocus. If enabled, trigger manual GC before/after focusing to report" + + " heap sizes reductions. This will increase the Skyfocus latency.") + public boolean dumpPostGcStats; +} diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java index b388983d8b7bf9..47f157164ac6c0 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuildCommand.java @@ -32,6 +32,7 @@ import com.google.devtools.build.lib.runtime.CommandEnvironment; import com.google.devtools.build.lib.runtime.KeepGoingOption; import com.google.devtools.build.lib.runtime.LoadingPhaseThreadsOption; +import com.google.devtools.build.lib.runtime.SkyfocusOptions; import com.google.devtools.build.lib.util.DetailedExitCode; import com.google.devtools.common.options.OptionsParsingResult; import java.util.List; @@ -52,7 +53,8 @@ LoadingOptions.class, KeepGoingOption.class, LoadingPhaseThreadsOption.class, - BuildEventProtocolOptions.class + BuildEventProtocolOptions.class, + SkyfocusOptions.class, }, usesConfigurationOptions = true, shortDescription = "Builds the specified targets.", diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/BuiltinCommandModule.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuiltinCommandModule.java index 054698c0ddb6d7..a5cef8db241569 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/BuiltinCommandModule.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/BuiltinCommandModule.java @@ -34,7 +34,6 @@ public void serverInit(OptionsParsingResult startupOptions, ServerBuilder builde new CleanCommand(), new CoverageCommand(), new DumpCommand(), - new FocusCommand(), new HelpCommand(), new InfoCommand(), new PrintActionCommand(), diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/FocusCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/FocusCommand.java deleted file mode 100644 index 29fafd7e955038..00000000000000 --- a/src/main/java/com/google/devtools/build/lib/runtime/commands/FocusCommand.java +++ /dev/null @@ -1,296 +0,0 @@ -// Copyright 2023 The Bazel Authors. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.build.lib.runtime.commands; - -import static com.google.common.collect.ImmutableSet.toImmutableSet; -import static java.util.stream.Collectors.toCollection; - -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Sets; -import com.google.devtools.build.lib.actions.Artifact; -import com.google.devtools.build.lib.collect.nestedset.ArtifactNestedSetKey; -import com.google.devtools.build.lib.events.Event; -import com.google.devtools.build.lib.events.Reporter; -import com.google.devtools.build.lib.pkgcache.PackageOptions; -import com.google.devtools.build.lib.profiler.Profiler; -import com.google.devtools.build.lib.profiler.SilentCloseable; -import com.google.devtools.build.lib.runtime.BlazeCommand; -import com.google.devtools.build.lib.runtime.BlazeCommandResult; -import com.google.devtools.build.lib.runtime.Command; -import com.google.devtools.build.lib.runtime.CommandEnvironment; -import com.google.devtools.build.lib.runtime.commands.info.UsedHeapSizeAfterGcInfoItem; -import com.google.devtools.build.lib.skyframe.PrecomputedValue; -import com.google.devtools.build.lib.skyframe.SkyframeExecutor; -import com.google.devtools.build.lib.util.InterruptedFailureDetails; -import com.google.devtools.build.lib.util.StringUtilities; -import com.google.devtools.build.lib.vfs.FileStateKey; -import com.google.devtools.build.lib.vfs.PathFragment; -import com.google.devtools.build.lib.vfs.Root; -import com.google.devtools.build.lib.vfs.RootedPath; -import com.google.devtools.build.skyframe.InMemoryGraphImpl; -import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; -import com.google.devtools.build.skyframe.SkyFunctionName; -import com.google.devtools.build.skyframe.SkyKey; -import com.google.devtools.build.skyframe.SkyframeFocuser; -import com.google.devtools.common.options.Converters; -import com.google.devtools.common.options.Option; -import com.google.devtools.common.options.OptionDocumentationCategory; -import com.google.devtools.common.options.OptionEffectTag; -import com.google.devtools.common.options.OptionsBase; -import com.google.devtools.common.options.OptionsParsingResult; -import java.io.PrintStream; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** The focus command. */ -@Command( - hidden = true, // experimental, don't show in the help command. - options = { - FocusCommand.FocusOptions.class, - PackageOptions.class, - }, - help = - "Usage: %{product} focus \n" - + "Reduces the memory usage of the %{product} JVM by promising that the user will only " - + "change a given set of files." - + "\n%{options}", - name = "focus", - shortDescription = "EXPERIMENTAL. Reduce memory usage with working sets.") -public class FocusCommand implements BlazeCommand { - - /** The set of options for the focus command. */ - public static class FocusOptions extends OptionsBase { - - @Option( - name = "experimental_working_set", - defaultValue = "", - effectTags = OptionEffectTag.HOST_MACHINE_RESOURCE_OPTIMIZATIONS, - // Deliberately undocumented. - documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, - converter = Converters.CommaSeparatedOptionListConverter.class, - help = "The working set. Specify as comma-separated workspace root-relative paths.") - public List workingSet; - - @Option( - name = "dump_keys", - defaultValue = "false", - effectTags = OptionEffectTag.TERMINAL_OUTPUT, - // Deliberately undocumented. - documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, - help = "Dump the focused SkyKeys.") - public boolean dumpKeys; - - @Option( - name = "dump_used_heap_size_after_gc", - defaultValue = "false", - effectTags = OptionEffectTag.TERMINAL_OUTPUT, - // Deliberately undocumented. - documentationCategory = OptionDocumentationCategory.UNDOCUMENTED, - help = - "If enabled, trigger manual GC before/after focusing to report accurate heap sizes. " - + "This will increase the focus command's latency.") - public boolean dumpUsedHeapSizeAfterGc; - } - - /** - * Reports the computed set of SkyKeys that need to be kept in the Skyframe graph for incremental - * correctness. - * - * @param reporter the event reporter - * @param focusResult the result from SkyframeFocuser - */ - private void dumpKeys(Reporter reporter, SkyframeFocuser.FocusResult focusResult) { - try (PrintStream pos = new PrintStream(reporter.getOutErr().getOutputStream())) { - pos.printf("Rdeps kept:\n"); - for (SkyKey key : focusResult.getRdeps()) { - pos.printf("%s", key.getCanonicalName()); - } - pos.println(); - pos.println("Deps kept:"); - for (SkyKey key : focusResult.getDeps()) { - pos.printf("%s", key.getCanonicalName()); - } - Map skyKeyCount = - Sets.union(focusResult.getRdeps(), focusResult.getDeps()).stream() - .collect(Collectors.groupingBy(SkyKey::functionName, Collectors.counting())); - - pos.println(); - pos.println("Summary of kept keys:"); - skyKeyCount.forEach((k, v) -> pos.println(k + " " + v)); - } - } - - private static void reportRequestStats( - CommandEnvironment env, FocusOptions focusOptions, Set roots, Set leafs) { - env.getReporter() - .handle( - Event.info( - String.format( - "Focusing on %d roots, %d leafs.. (use --dump_keys to show them)", - roots.size(), leafs.size()))); - if (focusOptions.dumpKeys) { - roots.forEach(k -> env.getReporter().handle(Event.info("root: " + k.getCanonicalName()))); - leafs.forEach(k -> env.getReporter().handle(Event.info("leaf: " + k.getCanonicalName()))); - } - } - - private static void reportResults( - CommandEnvironment env, - boolean dumpUsedHeapSize, - long beforeHeap, - int beforeNodeCount, - long afterHeap, - int afterNodeCount) { - StringBuilder results = new StringBuilder(); - if (dumpUsedHeapSize) { - // Users may skip heap size reporting, which triggers slow manual GCs, in place of faster - // focusing. - results.append( - String.format( - "Heap: %s -> %s (%.2f%% reduction), ", - StringUtilities.prettyPrintBytes(beforeHeap), - StringUtilities.prettyPrintBytes(afterHeap), - (double) (beforeHeap - afterHeap) / beforeHeap * 100)); - } - results.append( - String.format( - "Node count: %s -> %s (%.2f%% reduction)", - beforeNodeCount, - afterNodeCount, - (double) (beforeNodeCount - afterNodeCount) / beforeNodeCount * 100)); - env.getReporter().handle(Event.info(results.toString())); - } - - @Override - public BlazeCommandResult exec(CommandEnvironment env, OptionsParsingResult options) { - env.getReporter() - .handle( - Event.warn( - "The focus command is experimental. Feel free to test it, " - + "but do not depend on it yet.")); - - FocusOptions focusOptions = options.getOptions(FocusOptions.class); - - SkyframeExecutor executor = env.getSkyframeExecutor(); - // TODO: b/312819241 - add support for SerializationCheckingGraph for use in tests. - InMemoryMemoizingEvaluator evaluator = (InMemoryMemoizingEvaluator) executor.getEvaluator(); - InMemoryGraphImpl graph = (InMemoryGraphImpl) evaluator.getInMemoryGraph(); - - // Compute the roots and leafs. - // TODO: b/312819241 - find a less volatile way to specify roots. - Set roots = evaluator.getLatestTopLevelEvaluations(); - if (roots == null || roots.isEmpty()) { - env.getReporter().handle(Event.error("Unable to focus without roots. Run a build first.")); - // TODO: b/312819241 - turn this into a FailureDetail and avoid crashing. - throw new IllegalStateException("Unable to get root SkyKeys of the previous build."); - } - - // TODO: b/312819241 - For simplicity's sake, use the first --package_path as the root. - // This may be an issue with packages from a different package_path root. - Root packageRoot = env.getPackageLocator().getPathEntries().get(0); - HashSet workingSetRootedPaths = - focusOptions.workingSet.stream() - .map(f -> RootedPath.toRootedPath(packageRoot, PathFragment.create(f))) - .collect(toCollection(HashSet::new)); - - Set leafs = new LinkedHashSet<>(); - graph.parallelForEach( - node -> { - SkyKey k = node.getKey(); - if (k instanceof FileStateKey) { - RootedPath rootedPath = ((FileStateKey) k).argument(); - if (workingSetRootedPaths.remove(rootedPath)) { - leafs.add(k); - } - } - }); - if (leafs.isEmpty()) { - // TODO: b/312819241 - turn this into a FailureDetail and avoid crashing. - throw new IllegalStateException( - "Failed to construct working set because files not found in the transitive closure: " - + String.join(", ", focusOptions.workingSet)); - } - if (!workingSetRootedPaths.isEmpty()) { - env.getReporter() - .handle( - Event.warn( - workingSetRootedPaths.size() - + " files were not found in the transitive closure, and " - + "so they are not included in the working set.")); - } - - // TODO: b/312819241 - this leaf is necessary for build correctness of volatile actions, like - // stamping, but retains a lot of memory (100MB of retained heap for a 9+GB build). - leafs.add(PrecomputedValue.BUILD_ID.getKey()); // needed to invalidate linkstamped targets. - - reportRequestStats(env, focusOptions, roots, leafs); - - long beforeHeap = 0; - long afterHeap = 0; - - int beforeNodeCount = graph.valuesSize(); - if (focusOptions.dumpUsedHeapSizeAfterGc) { - beforeHeap = UsedHeapSizeAfterGcInfoItem.getHeapUsageAfterGc(); - } - - SkyframeFocuser.FocusResult focusResult; - try (SilentCloseable c = Profiler.instance().profile("SkyframeFocuser")) { - focusResult = - SkyframeFocuser.focus( - graph, - env.getReporter(), - roots, - leafs, - /* additionalDepsToKeep= */ (SkyKey k) -> { - // ActionExecutionFunction#lookupInput allows getting a transitive dep without - // adding a SkyframeDependency on it. In Blaze/Bazel's case, NestedSets are a major - // user. To keep that working, it's not sufficient to only keep the direct deps - // (e.g. - // NestedSets), but also keep the nodes of the transitive artifacts - // with this workaround. - if (k instanceof ArtifactNestedSetKey) { - return ((ArtifactNestedSetKey) k) - .expandToArtifacts().stream().map(Artifact::key).collect(toImmutableSet()); - } - return ImmutableSet.of(); - }); - } catch (InterruptedException e) { - return BlazeCommandResult.detailedExitCode( - InterruptedFailureDetails.detailedExitCode("focus interrupted")); - } - - int afterNodeCount = graph.valuesSize(); - if (focusOptions.dumpUsedHeapSizeAfterGc) { - afterHeap = UsedHeapSizeAfterGcInfoItem.getHeapUsageAfterGc(); - } - reportResults( - env, - focusOptions.dumpUsedHeapSizeAfterGc, - beforeHeap, - beforeNodeCount, - afterHeap, - afterNodeCount); - - if (focusOptions.dumpKeys) { - dumpKeys(env.getReporter(), focusResult); - } - - // Always succeeds (for now). - return BlazeCommandResult.success(); - } -} diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD index 93017fe52cb5b6..961c8ce90e000c 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/BUILD +++ b/src/main/java/com/google/devtools/build/lib/skyframe/BUILD @@ -254,7 +254,6 @@ java_library( "//src/main/java/com/google/devtools/build/lib/analysis:inconsistent_null_config_exception", "//src/main/java/com/google/devtools/build/lib/analysis:invalid_visibility_dependency_exception", "//src/main/java/com/google/devtools/build/lib/analysis:platform_configuration", - "//src/main/java/com/google/devtools/build/lib/analysis:platform_options", "//src/main/java/com/google/devtools/build/lib/analysis:required_config_fragments_provider", "//src/main/java/com/google/devtools/build/lib/analysis:resolved_toolchain_context", "//src/main/java/com/google/devtools/build/lib/analysis:starlark/starlark_build_settings_details_value", @@ -2396,6 +2395,27 @@ java_library( ], ) +java_library( + name = "skyfocus_module", + srcs = ["SkyfocusModule.java"], + deps = [ + ":precomputed_value", + "//src/main/java/com/google/devtools/build/lib:runtime", + "//src/main/java/com/google/devtools/build/lib/actions:artifacts", + "//src/main/java/com/google/devtools/build/lib/collect/nestedset:artifact_nested_set_key", + "//src/main/java/com/google/devtools/build/lib/events", + "//src/main/java/com/google/devtools/build/lib/profiler", + "//src/main/java/com/google/devtools/build/lib/runtime/commands/info", + "//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception", + "//src/main/java/com/google/devtools/build/lib/util:string", + "//src/main/java/com/google/devtools/build/lib/vfs", + "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment", + "//src/main/java/com/google/devtools/build/skyframe", + "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects", + "//third_party:guava", + ], +) + java_library( name = "bzl_load_cycle_reporter", srcs = ["BzlLoadCycleReporter.java"], diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyfocusModule.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyfocusModule.java new file mode 100644 index 00000000000000..c68f25e3d4e968 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyfocusModule.java @@ -0,0 +1,385 @@ +// Copyright 2024 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.skyframe; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toCollection; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.common.eventbus.Subscribe; +import com.google.devtools.build.lib.actions.Artifact; +import com.google.devtools.build.lib.buildtool.BuildPrecompleteEvent; +import com.google.devtools.build.lib.collect.nestedset.ArtifactNestedSetKey; +import com.google.devtools.build.lib.events.Event; +import com.google.devtools.build.lib.events.Reporter; +import com.google.devtools.build.lib.profiler.Profiler; +import com.google.devtools.build.lib.profiler.SilentCloseable; +import com.google.devtools.build.lib.runtime.BlazeModule; +import com.google.devtools.build.lib.runtime.CommandEnvironment; +import com.google.devtools.build.lib.runtime.SkyfocusOptions; +import com.google.devtools.build.lib.runtime.commands.info.UsedHeapSizeAfterGcInfoItem; +import com.google.devtools.build.lib.util.AbruptExitException; +import com.google.devtools.build.lib.util.StringUtilities; +import com.google.devtools.build.lib.vfs.FileStateKey; +import com.google.devtools.build.lib.vfs.PathFragment; +import com.google.devtools.build.lib.vfs.Root; +import com.google.devtools.build.lib.vfs.RootedPath; +import com.google.devtools.build.skyframe.InMemoryGraphImpl; +import com.google.devtools.build.skyframe.InMemoryMemoizingEvaluator; +import com.google.devtools.build.skyframe.SkyFunctionName; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.devtools.build.skyframe.SkyframeFocuser; +import com.google.devtools.build.skyframe.SkyframeFocuser.FocusResult; +import java.io.PrintStream; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * SkyfocusModule implements the concept of using working sets to reduce the memory footprint for + * incremental builds. + * + *

This is achieved with the `--experimental_working_set` build flag that takes in a + * comma-separated list of files, which defines the active working set. + * + *

Then, the active working set will be used to apply the optimizing algorithm when {@link + * BuildPrecompleteEvent} is fired, which is just before the build request stops. The core algorithm + * is implemented in {@link SkyframeFocuser}. + */ +public class SkyfocusModule extends BlazeModule { + + private enum PendingSkyfocusState { + // Blaze has to reset the evaluator state and restart analysis before running Skyfocus. This is + // usually due to Skyfocus having dropped nodes in a prior invocation, and there's no way to + // recover from it. This can be expensive. + RERUN_ANALYSIS_THEN_RUN_FOCUS, + + // Trigger Skyfocus. + RUN_FOCUS, + + DO_NOTHING + } + + private ImmutableSet activeWorkingSet = ImmutableSet.of(); + + private CommandEnvironment env; + + private PendingSkyfocusState pendingSkyfocusState; + + private SkyfocusOptions skyfocusOptions; + + @Override + public void beforeCommand(CommandEnvironment env) throws AbruptExitException { + // This should come before everything, as 'clean' would cause Blaze to drop its analysis + // state, therefore focusing needs to be re-done no matter what. + if (env.getCommandName().equals("clean")) { + activeWorkingSet = ImmutableSet.of(); + return; + } + + skyfocusOptions = env.getOptions().getOptions(SkyfocusOptions.class); + if (skyfocusOptions == null) { + // This is not a build command and is therefore a no-op as far as Skyfocus is concerned. + return; + } + + if (!env.getSkyframeExecutor().getEvaluator().skyfocusSupported()) { + activeWorkingSet = ImmutableSet.of(); + return; + } + + env.getSkyframeExecutor().getEvaluator().setSkyfocusEnabled(skyfocusOptions.skyfocusEnabled); + if (!skyfocusOptions.skyfocusEnabled) { + activeWorkingSet = ImmutableSet.of(); + return; + } + + // Allows this object to listen to build events. + env.getEventBus().register(this); + this.env = env; + + if (!activeWorkingSet.isEmpty()) { + env.getReporter() + .handle( + Event.info( + "Skyfocus is active. Changes not in the active working set are currently" + + " ignored.")); + } + + ImmutableSet newWorkingSet = ImmutableSet.copyOf(skyfocusOptions.workingSet); + pendingSkyfocusState = getPendingSkyfocusState(activeWorkingSet, newWorkingSet); + + switch (pendingSkyfocusState) { + case RERUN_ANALYSIS_THEN_RUN_FOCUS: + env.getReporter() + .handle( + Event.warn( + "Working set changed, discarding analysis cache. This can be expensive, " + + "so choose your working set carefully.")); + env.getSkyframeExecutor().resetEvaluator(); + // fall through + case RUN_FOCUS: + env.getReporter() + .handle( + Event.info( + "Updated working set successfully. Skyfocus will run at the end of the" + + " build.")); + activeWorkingSet = newWorkingSet; + env.getSkyframeExecutor().getEvaluator().setSkyfocusEnabled(true); + break; + case DO_NOTHING: + // Do not replace the active working set. + break; + } + } + + /** + * Compute the next state of Skyfocus using the active and new working set definitions. + * + *

TODO: b/323434582 - this should incorporate checking other forms of potential build + * incorrectness, like editing files outside of the working set. + */ + private static PendingSkyfocusState getPendingSkyfocusState( + Set activeWorkingSet, Set newWorkingSet) { + + // Skyfocus is not active. + if (activeWorkingSet.isEmpty()) { + if (newWorkingSet.isEmpty()) { + // No new working set is defined. Do nothing. + return PendingSkyfocusState.DO_NOTHING; + } else { + // New working set is defined. Run focus for the first time. + return PendingSkyfocusState.RUN_FOCUS; + } + } + + // activeWorkingSet is not empty, so Skyfocus is active. + if (newWorkingSet.isEmpty() || newWorkingSet.equals(activeWorkingSet)) { + // Unchanged working set. + return PendingSkyfocusState.DO_NOTHING; + } else if (activeWorkingSet.containsAll(newWorkingSet)) { + // New working set is a subset of the current working set. Refocus on the new working set and + // minimize the memory footprint further. + return PendingSkyfocusState.RUN_FOCUS; + } else { + // New working set contains new files. Unfortunately, this is a suboptimal path, and we + // have to re-run full analysis. + return PendingSkyfocusState.RERUN_ANALYSIS_THEN_RUN_FOCUS; + } + } + + /** + * Subscriber trigger for Skyfocus using {@link BuildPrecompleteEvent}. + * + *

This fires just before the build completes, which is the perfect time for applying Skyfocus. + * Skyfocus events should be profiled as part of the build command, so it should happen before the + * build completes or BuildTool request finishes. + */ + @SuppressWarnings("unused") + @Subscribe + public void onBuildPrecomplete(BuildPrecompleteEvent event) throws InterruptedException { + if (!skyfocusOptions.skyfocusEnabled) { + // Skyfocus not enabled, nothing to do here. + return; + } + + if (pendingSkyfocusState == PendingSkyfocusState.DO_NOTHING) { + // Skyfocus doesn't need to run, nothing to do here. + return; + } + + int beforeNodeCount = env.getSkyframeExecutor().getEvaluator().getValues().size(); + long beforeHeap = 0; + if (skyfocusOptions.dumpPostGcStats) { + beforeHeap = UsedHeapSizeAfterGcInfoItem.getHeapUsageAfterGc(); + } + + // Run Skyfocus! + FocusResult focusResult = focus(); + + // Shouldn't result in an empty graph. + Preconditions.checkState(!focusResult.getDeps().isEmpty()); + Preconditions.checkState(!focusResult.getRdeps().isEmpty()); + + if (skyfocusOptions.dumpKeys) { + dumpKeys(env.getReporter(), focusResult); + } + + reportNodeReduction( + env.getReporter(), + beforeNodeCount, + env.getSkyframeExecutor().getEvaluator().getValues().size()); + + if (skyfocusOptions.dumpPostGcStats) { + // Users may skip heap size reporting, which triggers slow manual GCs, in place of faster + // focusing. + reportHeapReduction( + env.getReporter(), beforeHeap, UsedHeapSizeAfterGcInfoItem.getHeapUsageAfterGc()); + } + + env.getSkyframeExecutor().getEvaluator().cleanupLatestTopLevelEvaluations(); + } + + /** The main entry point of the Skyfocus optimizations agains the Skyframe graph. */ + private FocusResult focus() throws InterruptedException { + // TODO: b/312819241 - add support for SerializationCheckingGraph for use in tests. + InMemoryMemoizingEvaluator evaluator = + (InMemoryMemoizingEvaluator) env.getSkyframeExecutor().getEvaluator(); + InMemoryGraphImpl graph = (InMemoryGraphImpl) evaluator.getInMemoryGraph(); + + Reporter reporter = env.getReporter(); + reporter.handle( + Event.warn("Skyfocus is experimental. Feel free to test it, but do not depend on it yet.")); + + // Compute the roots and leafs. + Set roots = evaluator.getLatestTopLevelEvaluations(); + if (roots == null || roots.isEmpty()) { + reporter.handle(Event.error("Unable to focus without roots. Run a build first.")); + // TODO: b/312819241 - turn this into a FailureDetail and avoid crashing. + throw new IllegalStateException("Unable to get root SkyKeys of the previous build."); + } + + // TODO: b/312819241 - For simplicity's sake, use the first --package_path as the root. + // This may be an issue with packages from a different package_path root. + Root packageRoot = env.getPackageLocator().getPathEntries().get(0); + HashSet workingSetRootedPaths = + activeWorkingSet.stream() + .map(f -> RootedPath.toRootedPath(packageRoot, PathFragment.create(f))) + .collect(toCollection(HashSet::new)); + + Set leafs = new LinkedHashSet<>(); + graph.parallelForEach( + node -> { + SkyKey k = node.getKey(); + if (k instanceof FileStateKey) { + RootedPath rootedPath = ((FileStateKey) k).argument(); + if (workingSetRootedPaths.remove(rootedPath)) { + leafs.add(k); + } + } + }); + if (leafs.isEmpty()) { + // TODO: b/312819241 - turn this into a FailureDetail and avoid crashing. + throw new IllegalStateException( + "Failed to construct working set because none of the files in the working set are found" + + " in the transitive closure of the build."); + } + if (!workingSetRootedPaths.isEmpty()) { + reporter.handle( + Event.warn( + workingSetRootedPaths.size() + + " files were not found in the transitive closure, and " + + "so they are not included in the working set. They are: " + + workingSetRootedPaths.stream() + .map(r -> r.getRootRelativePath().toString()) + .collect(joining(", ")))); + } + + // TODO: b/312819241 - this leaf is necessary for build correctness of volatile actions, like + // stamping, but retains a lot of memory (100MB of retained heap for a 9+GB build). + leafs.add(PrecomputedValue.BUILD_ID.getKey()); // needed to invalidate linkstamped targets. + + reporter.handle( + Event.info( + String.format( + "Focusing on %d roots, %d leafs.. (use --dump_keys to show them)", + roots.size(), leafs.size()))); + + FocusResult focusResult; + + try (SilentCloseable c = Profiler.instance().profile("SkyframeFocuser")) { + focusResult = + SkyframeFocuser.focus( + graph, + reporter, + roots, + leafs, + /* additionalDepsToKeep= */ (SkyKey k) -> { + // ActionExecutionFunction#lookupInput allows getting a transitive dep without + // adding a SkyframeDependency on it. In Blaze/Bazel's case, NestedSets are a major + // user. To keep that working, it's not sufficient to only keep the direct deps + // (e.g. NestedSets), but also keep the nodes of the transitive artifacts + // with this workaround. + if (k instanceof ArtifactNestedSetKey) { + return ((ArtifactNestedSetKey) k) + .expandToArtifacts().stream().map(Artifact::key).collect(toImmutableSet()); + } + return ImmutableSet.of(); + }); + } + + return focusResult; + } + + private static void reportNodeReduction( + Reporter reporter, int beforeNodeCount, int afterNodeCount) { + reporter.handle( + Event.info( + String.format( + "Node count: %s -> %s (%.2f%% reduction)", + beforeNodeCount, + afterNodeCount, + (double) (beforeNodeCount - afterNodeCount) / beforeNodeCount * 100))); + } + + private static void reportHeapReduction(Reporter reporter, long beforeHeap, long afterHeap) { + reporter.handle( + Event.info( + String.format( + "Heap: %s -> %s (%.2f%% reduction), ", + StringUtilities.prettyPrintBytes(beforeHeap), + StringUtilities.prettyPrintBytes(afterHeap), + (double) (beforeHeap - afterHeap) / beforeHeap * 100))); + } + + /** + * Reports the computed set of SkyKeys that need to be kept in the Skyframe graph for incremental + * correctness. + * + * @param reporter the event reporter + * @param focusResult the result from SkyframeFocuser + */ + private static void dumpKeys(Reporter reporter, SkyframeFocuser.FocusResult focusResult) { + try (PrintStream pos = new PrintStream(reporter.getOutErr().getOutputStream())) { + focusResult + .getRoots() + .forEach(k -> reporter.handle(Event.info("root: " + k.getCanonicalName()))); + focusResult + .getLeafs() + .forEach(k -> reporter.handle(Event.info("leaf: " + k.getCanonicalName()))); + + pos.printf("Rdeps kept:\n"); + for (SkyKey key : focusResult.getRdeps()) { + pos.printf("%s", key.getCanonicalName()); + } + pos.println(); + pos.println("Deps kept:"); + for (SkyKey key : focusResult.getDeps()) { + pos.printf("%s", key.getCanonicalName()); + } + Map skyKeyCount = + Sets.union(focusResult.getRdeps(), focusResult.getDeps()).stream() + .collect(Collectors.groupingBy(SkyKey::functionName, Collectors.counting())); + + pos.println(); + pos.println("Summary of kept keys:"); + skyKeyCount.forEach((k, v) -> pos.println(k + " " + v)); + } + } +} 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 2db2f67d55d17d..d6d4bd35bbdc57 100644 --- a/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java +++ b/src/main/java/com/google/devtools/build/skyframe/InMemoryMemoizingEvaluator.java @@ -43,18 +43,8 @@ public final class InMemoryMemoizingEvaluator extends AbstractInMemoryMemoizingE private final AtomicBoolean evaluating = new AtomicBoolean(false); - private Set latestTopLevelEvaluations; - private Set topLevelEvaluations = new HashSet<>(); - - @Override - public void updateTopLevelEvaluations() { - latestTopLevelEvaluations = topLevelEvaluations; - topLevelEvaluations = new HashSet<>(); - } - - public Set getLatestTopLevelEvaluations() { - return latestTopLevelEvaluations; - } + private Set latestTopLevelEvaluations = new HashSet<>(); + private boolean skyfocusEnabled; public InMemoryMemoizingEvaluator( Map skyFunctions, Differencer differencer) { @@ -107,10 +97,10 @@ public EvaluationResult evaluate( Version graphVersion = getNextGraphVersion(); setAndCheckEvaluateState(true, roots); - // Only remember roots for focusing if we're tracking incremental states by keeping edges. - if (keepEdges) { - // Remember the top level evaluation of the last build-like invocation. - Iterables.addAll(topLevelEvaluations, roots); + // Only remember roots for Skyfocus if we're tracking incremental states by keeping edges. + if (keepEdges && skyfocusEnabled) { + // Remember the top level evaluation of the build invocation for post-build consumption. + Iterables.addAll(latestTopLevelEvaluations, roots); } // Mark for removal any nodes from the previous evaluation that were still inflight or were @@ -192,11 +182,30 @@ public void injectGraphTransformerForTesting(GraphTransformerForTesting transfor this.graph = transformer.transform(this.graph); } + @Override + public boolean skyfocusSupported() { + return true; + } + @Override public InMemoryGraph getInMemoryGraph() { return graph; } + public Set getLatestTopLevelEvaluations() { + return latestTopLevelEvaluations; + } + + @Override + public void cleanupLatestTopLevelEvaluations() { + latestTopLevelEvaluations = new HashSet<>(); + } + + @Override + public void setSkyfocusEnabled(boolean enabled) { + this.skyfocusEnabled = enabled; + } + public ImmutableMap getSkyFunctionsForTesting() { return skyFunctions; } diff --git a/src/main/java/com/google/devtools/build/skyframe/MemoizingEvaluator.java b/src/main/java/com/google/devtools/build/skyframe/MemoizingEvaluator.java index af29dba583988a..db0c10ca2aa9c3 100644 --- a/src/main/java/com/google/devtools/build/skyframe/MemoizingEvaluator.java +++ b/src/main/java/com/google/devtools/build/skyframe/MemoizingEvaluator.java @@ -254,11 +254,11 @@ public ProcessableGraph transform(ProcessableGraph graph) { */ void cleanupInterningPools(); - /** - * Implementations of MemoizingEvaluator can choose to remember the top level SkyKeys evaluated - * from a previous build for further optimizations, like the focus command. - * - *

This can be called to purge an existing set of SkyKeys, and replace it with a new set. - */ - void updateTopLevelEvaluations(); + boolean skyfocusSupported(); + + /** Enables Skyfocus, a graph optimizer for Skyframe with working sets. */ + void setSkyfocusEnabled(boolean enabled); + + /** Cleans up the set of evaluated root SkyKeys. Used for Skyfocus. */ + void cleanupLatestTopLevelEvaluations(); } diff --git a/src/main/java/com/google/devtools/build/skyframe/SkyframeFocuser.java b/src/main/java/com/google/devtools/build/skyframe/SkyframeFocuser.java index 8d60f29d96dfcc..7a80f79e722298 100644 --- a/src/main/java/com/google/devtools/build/skyframe/SkyframeFocuser.java +++ b/src/main/java/com/google/devtools/build/skyframe/SkyframeFocuser.java @@ -94,14 +94,33 @@ public static FocusResult focus( */ public static class FocusResult { + private final ImmutableSet roots; + + private final ImmutableSet leafs; + private final ImmutableSet rdeps; + private final ImmutableSet deps; - private FocusResult(ImmutableSet rdeps, ImmutableSet deps) { + private FocusResult( + ImmutableSet roots, + ImmutableSet leafs, + ImmutableSet rdeps, + ImmutableSet deps) { + this.roots = roots; + this.leafs = leafs; this.rdeps = rdeps; this.deps = deps; } + public ImmutableSet getRoots() { + return roots; + } + + public ImmutableSet getLeafs() { + return leafs; + } + /** * Returns the set of SkyKeys that are in the dependencies of all roots, and rdeps from the * leafs. May contain transitive dependencies, in cases where certain functions use them without @@ -296,6 +315,10 @@ private FocusResult run( Event.info( String.format("Rdep edges: %s -> %s", rdepEdgesBefore.get(), rdepEdgesAfter.get()))); - return new FocusResult(ImmutableSet.copyOf(keptRdeps), ImmutableSet.copyOf(keptDeps)); + return new FocusResult( + ImmutableSet.copyOf(roots), + ImmutableSet.copyOf(leafs), + ImmutableSet.copyOf(keptRdeps), + ImmutableSet.copyOf(keptDeps)); } } diff --git a/src/test/shell/integration/focus_test.sh b/src/test/shell/integration/focus_test.sh index fee024ffee5433..a13ce6ab64ac16 100755 --- a/src/test/shell/integration/focus_test.sh +++ b/src/test/shell/integration/focus_test.sh @@ -14,7 +14,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# An end-to-end test for the 'focus' command. +# An end-to-end test for Skyfocus & working sets. # --- begin runfiles.bash initialization --- set -euo pipefail @@ -55,6 +55,8 @@ if "$is_windows"; then export MSYS2_ARG_CONV_EXCL="*" fi +add_to_bazelrc "build --experimental_enable_skyfocus" + function set_up() { # Ensure we always start with a fresh server so that the following # env vars are picked up on startup. This could also be `bazel shutdown`, @@ -67,7 +69,7 @@ function set_up() { export DONT_SANITY_CHECK_SERIALIZATION=1 } -function test_must_be_used_after_a_build() { +function test_working_set_can_be_used_with_build_command() { local -r pkg=${FUNCNAME[0]} mkdir ${pkg}|| fail "cannot mkdir ${pkg}" mkdir -p ${pkg} @@ -81,8 +83,10 @@ genrule( ) EOF - bazel focus --experimental_working_set=${pkg}/in.txt >$TEST_log 2>&1 && "unexpected success" - expect_log "Unable to focus without roots. Run a build first." + bazel build //${pkg}:g \ + --experimental_working_set=${pkg}/in.txt >$TEST_log 2>&1 \ + || "unexpected failure" + expect_log "Focusing on" } function test_correctly_rebuilds_after_using_focus() { @@ -101,14 +105,16 @@ EOF out=$(bazel info "${PRODUCT_NAME}-genfiles")/${pkg}/out.txt - bazel build //${pkg}:g + bazel build //${pkg}:g --experimental_working_set=${pkg}/in.txt assert_contains "input" $out - echo "a change" >> ${pkg}/in.txt - bazel focus --experimental_working_set=${pkg}/in.txt + echo "first change" >> ${pkg}/in.txt + bazel build //${pkg}:g --experimental_working_set=${pkg}/in.txt + assert_contains "first change" $out + echo "second change" >> ${pkg}/in.txt bazel build //${pkg}:g - assert_contains "a change" $out + assert_contains "second change" $out } function test_focus_command_prints_info_about_graph() { @@ -126,10 +132,8 @@ genrule( EOF out=$(bazel info "${PRODUCT_NAME}-genfiles")/${pkg}/out.txt - bazel build //${pkg}:g - - bazel focus \ - --dump_used_heap_size_after_gc \ + bazel build //${pkg}:g\ + --experimental_skyfocus_dump_post_gc_stats \ --experimental_working_set=${pkg}/in.txt >$TEST_log 2>&1 expect_log "Focusing on .\+ roots, .\+ leafs" @@ -155,9 +159,9 @@ genrule( EOF out=$(bazel info "${PRODUCT_NAME}-genfiles")/${pkg}/out.txt - bazel build //${pkg}:g &> $TEST_log 2>&1 - - bazel focus --dump_keys --experimental_working_set=${pkg}/in.txt >$TEST_log 2>&1 + bazel build //${pkg}:g \ + --experimental_skyfocus_dump_keys \ + --experimental_working_set=${pkg}/in.txt >$TEST_log 2>&1 expect_log "Focusing on .\+ roots, .\+ leafs" @@ -197,17 +201,68 @@ genrule( ) EOF - outdir=$(bazel info "${PRODUCT_NAME}-genfiles")/${pkg} - bazel build //${pkg}:g echo "a change" >> ${pkg}/in.txt - bazel focus --experimental_working_set=${pkg}/in.txt + bazel build //${pkg}:g \ + --experimental_working_set=${pkg}/in.txt bazel build //${pkg}:g bazel build //${pkg}:g2 || fail "cannot build //${pkg}:g2" bazel build //${pkg}:g3 || fail "cannot build //${pkg}:g3" } +function test_working_set_can_be_reduced_without_reanalysis() { + local -r pkg=${FUNCNAME[0]} + mkdir ${pkg}|| fail "cannot mkdir ${pkg}" + mkdir -p ${pkg} + echo "input1" > ${pkg}/in.txt + echo "input2" > ${pkg}/in2.txt + cat > ${pkg}/BUILD <> ${pkg}/in.txt + bazel build //${pkg}:g --experimental_working_set=${pkg}/in.txt &> "$TEST_log" + assert_contains "a change" $out + expect_not_log "discarding analysis cache" +} + +function test_working_set_expansion_causes_reanalysis() { + local -r pkg=${FUNCNAME[0]} + mkdir ${pkg}|| fail "cannot mkdir ${pkg}" + mkdir -p ${pkg} + echo "input1" > ${pkg}/in.txt + echo "input2" > ${pkg}/in2.txt + cat > ${pkg}/BUILD <> ${pkg}/in2.txt + bazel build //${pkg}:g --experimental_working_set=${pkg}/in.txt,${pkg}/in2.txt &> "$TEST_log" + assert_contains "a change" $out + expect_log "discarding analysis cache" +} + function test_focus_emits_profile_data() { local -r pkg=${FUNCNAME[0]} mkdir ${pkg}|| fail "cannot mkdir ${pkg}" @@ -222,8 +277,8 @@ genrule( ) EOF - bazel build //${pkg}:g - bazel focus --experimental_working_set=${pkg}/in.txt \ + bazel build //${pkg}:g \ + --experimental_working_set=${pkg}/in.txt \ --profile=/tmp/profile.log &> "$TEST_log" || fail "Expected success" grep '"ph":"X"' /tmp/profile.log > "$TEST_log" \ || fail "Missing profile file."