diff options
5 files changed, 372 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java index 927355d9a9..4d2cdcef78 100644 --- a/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java +++ b/src/main/java/com/google/devtools/build/lib/runtime/BuildSummaryStatsModule.java @@ -18,6 +18,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import com.google.devtools.build.lib.actions.ActionKeyContext; +import com.google.devtools.build.lib.actions.ActionResultReceivedEvent; import com.google.devtools.build.lib.buildeventstream.BuildToolLogs; import com.google.devtools.build.lib.buildtool.BuildRequest; import com.google.devtools.build.lib.buildtool.buildevent.BuildCompleteEvent; @@ -50,11 +51,14 @@ public class BuildSummaryStatsModule extends BlazeModule { private boolean enabled; private boolean discardActions; + private SpawnStats spawnStats; + @Override public void beforeCommand(CommandEnvironment env) { this.reporter = env.getReporter(); this.eventBus = env.getEventBus(); this.actionKeyContext = env.getSkyframeExecutor().getActionKeyContext(); + this.spawnStats = new SpawnStats(); eventBus.register(this); } @@ -63,6 +67,7 @@ public class BuildSummaryStatsModule extends BlazeModule { this.criticalPathComputer = null; this.eventBus = null; this.reporter = null; + this.spawnStats = null; } @Override @@ -81,6 +86,11 @@ public class BuildSummaryStatsModule extends BlazeModule { } @Subscribe + public void actionResultReceived(ActionResultReceivedEvent event) { + spawnStats.countActionResult(event.getActionResult()); + } + + @Subscribe public void buildComplete(BuildCompleteEvent event) { try { // We might want to make this conditional on a flag; it can sometimes be a bit of a nuisance. @@ -115,6 +125,11 @@ public class BuildSummaryStatsModule extends BlazeModule { } reporter.handle(Event.info(Joiner.on(", ").join(items))); + + String spawnSummary = spawnStats.getSummary(); + reporter.handle(Event.info(spawnSummary)); + statistics.add(Pair.of("process stats", ByteString.copyFromUtf8(spawnSummary))); + reporter.post(new BuildToolLogs(statistics, ImmutableList.of())); } finally { criticalPathComputer = null; diff --git a/src/main/java/com/google/devtools/build/lib/runtime/SpawnStats.java b/src/main/java/com/google/devtools/build/lib/runtime/SpawnStats.java new file mode 100644 index 0000000000..dcb68ca8a9 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/runtime/SpawnStats.java @@ -0,0 +1,106 @@ +// Copyright 2018 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.common.collect.ConcurrentHashMultiset; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Multiset; +import com.google.devtools.build.lib.actions.ActionResult; +import com.google.devtools.build.lib.actions.SpawnResult; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import javax.annotation.concurrent.ThreadSafe; + +/** Collects results from SpawnResult. */ +@ThreadSafe +class SpawnStats { + private final ConcurrentHashMultiset<String> runners = ConcurrentHashMultiset.create(); + private static final ImmutableList<String> REPORT_FIRST = ImmutableList.of("remote cache hit"); + + public void countActionResult(ActionResult actionResult) { + for (SpawnResult r : actionResult.spawnResults()) { + countRunnerName(r.getRunnerName()); + } + } + + public void countRunnerName(String runner) { + runners.add(runner); + } + + private static class ResultString { + StringBuilder result = new StringBuilder(); + String firstRunner; + int spawnsCount = 0; + int runnersNum = 0; + + public int spawnsCount() { + return spawnsCount; + } + + public void add(String name, int count) { + spawnsCount += count; + runnersNum += 1; + + if (runnersNum == 1) { + firstRunner = name; + } + + if (result.length() > 0) { + result.append(", "); + } + result.append(count); + result.append(" "); + result.append(name); + } + + @Override + public String toString() { + if (runnersNum == 0) { + return ""; + } + if (runnersNum == 1) { + return ", " + firstRunner; + } + return ": " + result; + } + } + + /* + * Returns a human-readable summary of spawns counted. + */ + public String getSummary() { + ResultString result = new ResultString(); + + // First report cache results. + for (String s : REPORT_FIRST) { + int count = runners.setCount(s, 0); + if (count > 0) { + result.add(s, count); + } + } + + // Sort the rest alphabetically + ArrayList<Multiset.Entry<String>> list = new ArrayList<>(runners.entrySet()); + Collections.sort(list, Comparator.comparing(e -> e.getElement())); + + for (Multiset.Entry<String> e : list) { + result.add(e.getElement(), e.getCount()); + } + + int total = result.spawnsCount(); + return total + " process" + (total == 1 ? "" : "es") + result + "."; + } +} diff --git a/src/test/java/com/google/devtools/build/lib/runtime/SpawnStatsTest.java b/src/test/java/com/google/devtools/build/lib/runtime/SpawnStatsTest.java new file mode 100644 index 0000000000..968108db64 --- /dev/null +++ b/src/test/java/com/google/devtools/build/lib/runtime/SpawnStatsTest.java @@ -0,0 +1,160 @@ +// Copyright 2018 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 static com.google.common.truth.Truth.assertThat; + +import com.google.devtools.build.lib.actions.ActionResult; +import com.google.devtools.build.lib.actions.SpawnResult; +import java.util.ArrayList; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Testing SpawnStats */ +@RunWith(JUnit4.class) +public final class SpawnStatsTest { + + SpawnStats stats; + + @Before + public void setUp() { + stats = new SpawnStats(); + } + + @Test + public void emptySet() { + assertThat(stats.getSummary()).isEqualTo("0 processes."); + } + + @Test + public void one() { + stats.countRunnerName("foo"); + assertThat(stats.getSummary()).isEqualTo("1 process, foo."); + } + + @Test + public void oneRemote() { + stats.countRunnerName("remote cache hit"); + assertThat(stats.getSummary()).isEqualTo("1 process, remote cache hit."); + } + + @Test + public void two() { + stats.countRunnerName("foo"); + stats.countRunnerName("foo"); + assertThat(stats.getSummary()).isEqualTo("2 processes, foo."); + } + + @Test + public void order() { + stats.countRunnerName("a"); + stats.countRunnerName("b"); + stats.countRunnerName("b"); + stats.countRunnerName("c"); + stats.countRunnerName("c"); + stats.countRunnerName("c"); + assertThat(stats.getSummary()).isEqualTo("6 processes: 1 a, 2 b, 3 c."); + } + + @Test + public void reverseOrder() { + stats.countRunnerName("a"); + stats.countRunnerName("a"); + stats.countRunnerName("a"); + stats.countRunnerName("b"); + stats.countRunnerName("b"); + stats.countRunnerName("c"); + assertThat(stats.getSummary()).isEqualTo("6 processes: 3 a, 2 b, 1 c."); + } + + @Test + public void cacheFirst() { + stats.countRunnerName("a"); + stats.countRunnerName("a"); + stats.countRunnerName("a"); + stats.countRunnerName("b"); + stats.countRunnerName("remote cache hit"); + stats.countRunnerName("b"); + stats.countRunnerName("c"); + assertThat(stats.getSummary()).isEqualTo("7 processes: 1 remote cache hit, 3 a, 2 b, 1 c."); + } + + private final SpawnResult rA = + new SpawnResult.Builder().setStatus(SpawnResult.Status.SUCCESS).setRunnerName("abc").build(); + private final SpawnResult rB = + new SpawnResult.Builder().setStatus(SpawnResult.Status.SUCCESS).setRunnerName("cde").build(); + + @Test + public void actionOneSpawn() { + + ArrayList<SpawnResult> spawns = new ArrayList<>(); + spawns.add(rA); + + stats.countActionResult(ActionResult.create(spawns)); + assertThat(stats.getSummary()).isEqualTo("1 process, abc."); + } + + @Test + public void actionManySpawn() { + // Different spawns with the same runner count as one action + + ArrayList<SpawnResult> spawns = new ArrayList<>(); + spawns.add(rA); + spawns.add(rA); + spawns.add(rA); + + stats.countActionResult(ActionResult.create(spawns)); + assertThat(stats.getSummary()).isEqualTo("3 processes, abc."); + } + + @Test + public void actionManySpawnMixed() { + // Different spawns mixed runners + + ArrayList<SpawnResult> spawns = new ArrayList<>(); + spawns.add(rA); + spawns.add(rA); + spawns.add(rB); + + stats.countActionResult(ActionResult.create(spawns)); + assertThat(stats.getSummary()).isEqualTo("3 processes: 2 abc, 1 cde."); + } + + @Test + public void actionManyActionsMixed() { + // Five actions: + // abc + // abc, abc + // abc, abc, cde + // abc, abc, cde + // abc, abc, cde + + ArrayList<SpawnResult> spawns = new ArrayList<>(); + spawns.add(rA); + stats.countActionResult(ActionResult.create(spawns)); + + spawns.add(rA); + stats.countActionResult(ActionResult.create(spawns)); + + spawns.add(rB); + stats.countActionResult(ActionResult.create(spawns)); + stats.countActionResult(ActionResult.create(spawns)); + stats.countActionResult(ActionResult.create(spawns)); + + assertThat(stats.getSummary()).isEqualTo("12 processes: 9 abc, 3 cde."); + } +} diff --git a/src/test/shell/bazel/BUILD b/src/test/shell/bazel/BUILD index d2c79390ad..9aab738386 100644 --- a/src/test/shell/bazel/BUILD +++ b/src/test/shell/bazel/BUILD @@ -154,6 +154,12 @@ sh_test( ) sh_test( + name = "bazel_spawnstats_test", + srcs = ["bazel_spawnstats_test.sh"], + data = [":test-deps"], +) + +sh_test( name = "bazel_coverage_test", srcs = ["bazel_coverage_test.sh"], data = [":test-deps"], diff --git a/src/test/shell/bazel/bazel_spawnstats_test.sh b/src/test/shell/bazel/bazel_spawnstats_test.sh new file mode 100755 index 0000000000..958296f47f --- /dev/null +++ b/src/test/shell/bazel/bazel_spawnstats_test.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# +# Copyright 2018 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. +# +# Test lightweight spawn stats generation in Bazel +# + +set -eu + +# Load the test setup defined in the parent directory +CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${CURRENT_DIR}/../integration_test_setup.sh" \ + || { echo "integration_test_setup.sh not found!" >&2; exit 1; } + +function set_up() { + cat > BUILD <<EOF +genrule( + name = "foo", + cmd = "echo hello > \$@", + outs = ["foo.txt"], +) +EOF +} + +function test_order() { + # Ensure the new stats are printed before Build completed + bazel build :foo 2>&1 | tee ${TEST_log} | sed -n '/process/,$p' | grep "Build complete" || fail "Expected \"process\" to be followed by \"Build completed\"" +} + +# Single execution of Bazel +function statistics_single() { + flags=$1 # flags to pass to Bazel + expect=$2 # string to expect + + echo "Starting single run for $flags $expect" &> $TEST_log + output=`bazel build :foo $flags 2>&1 | tee ${TEST_log} | grep " process" | tr -d '\r'` + + if ! [[ $output =~ ${expect} ]]; then + fail "bazel ${flags}: Want |${expect}|, got |${output}| " + fi + + echo "Done $flags $expect" &> $TEST_log +} + +function test_local() { + statistics_single "--spawn_strategy=local" ", local" +} + +function test_local_sandbox() { + if [[ "$PLATFORM" == "linux" ]]; then + statistics_single "--spawn_strategy=linux-sandbox" ", linux-sandbox" + fi +} + +# We are correctly resetting the counts +function test_repeat() { + flags="--spawn_strategy=local" + statistics_single $flags ", local" + bazel clean $flags + statistics_single $flags ", local" +} + +# Locally cached results are not yet displayed +function test_localcache() { + flags="--spawn_strategy=local" + # We are correctly resetting the counts + statistics_single $flags ", local" + statistics_single $flags "0 processes." +} + +run_suite "bazel statistics tests" + + |