Open source MetricsCollectorTest.

PiperOrigin-RevId: 427686246
diff --git a/src/test/java/com/google/devtools/build/lib/metrics/BUILD b/src/test/java/com/google/devtools/build/lib/metrics/BUILD
index 133fa2f..09b20c3 100644
--- a/src/test/java/com/google/devtools/build/lib/metrics/BUILD
+++ b/src/test/java/com/google/devtools/build/lib/metrics/BUILD
@@ -15,7 +15,7 @@
 java_test(
     name = "PostGCMemoryUseRecorderTest",
     size = "small",
-    srcs = glob(["*.java"]),
+    srcs = ["PostGCMemoryUseRecorderTest.java"],
     deps = [
         "//src/main/java/com/google/devtools/build/lib/metrics:memory-use-recorder",
         "//src/test/java/com/google/devtools/build/lib/testutil",
@@ -26,3 +26,21 @@
         "//third_party:truth",
     ],
 )
+
+java_test(
+    name = "MetricsCollectorTest",
+    srcs = ["MetricsCollectorTest.java"],
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:runtime",
+        "//src/main/java/com/google/devtools/build/lib/actions",
+        "//src/main/java/com/google/devtools/build/lib/analysis:view_creation_failed_exception",
+        "//src/main/java/com/google/devtools/build/lib/buildeventstream/proto:build_event_stream_java_proto",
+        "//src/main/java/com/google/devtools/build/lib/metrics:event",
+        "//src/main/java/com/google/devtools/build/lib/metrics:metrics_module",
+        "//src/main/java/com/google/devtools/build/lib/util:os",
+        "//src/test/java/com/google/devtools/build/lib/buildtool/util",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
diff --git a/src/test/java/com/google/devtools/build/lib/metrics/MetricsCollectorTest.java b/src/test/java/com/google/devtools/build/lib/metrics/MetricsCollectorTest.java
new file mode 100644
index 0000000..3690e8a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/lib/metrics/MetricsCollectorTest.java
@@ -0,0 +1,580 @@
+// Copyright 2022 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.metrics;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.eventbus.Subscribe;
+import com.google.devtools.build.lib.actions.BuildFailedException;
+import com.google.devtools.build.lib.analysis.ViewCreationFailedException;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildMetrics;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildMetrics.ActionSummary.ActionData;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildMetrics.ArtifactMetrics;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildMetrics.BuildGraphMetrics;
+import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.BuildMetrics.CumulativeMetrics;
+import com.google.devtools.build.lib.buildtool.util.BuildIntegrationTestCase;
+import com.google.devtools.build.lib.runtime.BlazeModule;
+import com.google.devtools.build.lib.runtime.BlazeRuntime;
+import com.google.devtools.build.lib.runtime.CommandEnvironment;
+import com.google.devtools.build.lib.util.OS;
+import java.util.List;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests metric collection. */
+@RunWith(JUnit4.class)
+public class MetricsCollectorTest extends BuildIntegrationTestCase {
+
+  static class BuildMetricsEventListener extends BlazeModule {
+
+    private BuildMetricsEvent event;
+
+    @Override
+    public void beforeCommand(CommandEnvironment env) {
+      env.getEventBus().register(this);
+    }
+
+    @Subscribe
+    public void onBuildMetrics(BuildMetricsEvent event) {
+      this.event = event;
+    }
+  }
+
+  private BuildMetricsEventListener buildMetricsEventListener = new BuildMetricsEventListener();
+
+  @Override
+  protected BlazeRuntime.Builder getRuntimeBuilder() throws Exception {
+    return super.getRuntimeBuilder()
+        .addBlazeModule(new MetricsModule())
+        .addBlazeModule(buildMetricsEventListener);
+  }
+
+  @Before
+  public void writeTrivialFooTarget() throws Exception {
+    write(
+        "foo/BUILD",
+        "genrule(",
+        "    name = 'foo',",
+        "    outs = ['dir'],",
+        "    cmd = '/bin/mkdir $(location dir)',",
+        "    srcs = [],",
+        ")");
+  }
+
+  @Test
+  public void testActionsCreated() throws Exception {
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getActionSummary().getActionsCreated()).isGreaterThan(0L);
+  }
+
+  @Test
+  public void testActionsCreatedIsZeroOnSecondBuild() throws Exception {
+    buildTarget("//foo:foo");
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getActionSummary().getActionsCreated()).isEqualTo(0);
+  }
+
+  @Test
+  public void testActionsExecuted() throws Exception {
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getActionSummary().getActionsExecuted()).isGreaterThan(0L);
+  }
+
+  @Test
+  public void testActionsExecutedIsZeroOnSecondBuild() throws Exception {
+    buildTarget("//foo:foo");
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getActionSummary().getActionsExecuted()).isEqualTo(0);
+  }
+
+  @Test
+  public void buildGraphAndArtifactMetrics() throws Exception {
+    write(
+        "a/BUILD",
+        "genrule(name = 'a', srcs = ['//b:b', '//b:c'], outs = ['a.out'], cmd = 'cat $(SRCS) >"
+            + " $@')");
+    write(
+        "b/BUILD",
+        "genrule(name = 'b', srcs = ['b.in', 'c.in'], outs = ['b.out'], cmd = 'cat $(SRCS) > $@')",
+        "genrule(name = 'c', srcs = ['c.in', 'c2.in'], outs = ['c.out'], cmd = 'cat $(SRCS) >"
+            + " $@')");
+    if (OS.getCurrent() == OS.WINDOWS) {
+      // On Windows we have \r\n line endings while on other platforms only \n. So make the file one
+      // byte shorter on Windows so that the byte counts below match.
+      write("b/b.in", "1234");
+      write("b/c.in", "1");
+    } else {
+      write("b/b.in", "12345");
+      write("b/c.in", "12");
+    }
+    createSymlink("c.in", "b/c2.in");
+    write(
+        "e/BUILD",
+        "alias(name = 'facade', actual = ':e.out')",
+        "genrule(name = 'e', srcs = ['e.in'], outs = ['e.out'], cmd = 'cat $(SRCS) > $@')");
+    if (OS.getCurrent() == OS.WINDOWS) {
+      // On Windows we have \r\n line endings while on other platforms only \n. So make the file one
+      // byte shorter on Windows so that the byte counts below match.
+      write("e/e.in", "ab");
+    } else {
+      write("e/e.in", "abc");
+    }
+
+    // Do one build of a target in a standalone package. Gets us a baseline for analysis/execution.
+    buildTarget("//e:facade");
+    BuildGraphMetrics buildGraphMetrics =
+        buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics();
+    int actionLookupValueCount = buildGraphMetrics.getActionLookupValueCount();
+    // All these numbers should be big, but just want a basic check.
+    assertThat(actionLookupValueCount).isGreaterThan(0);
+    assertThat(buildGraphMetrics.getActionLookupValueCountNotIncludingAspects())
+        .isEqualTo(actionLookupValueCount);
+    int actionCount = buildGraphMetrics.getActionCount();
+    assertThat(actionCount).isGreaterThan(0);
+    assertThat(buildGraphMetrics.getActionCountNotIncludingAspects()).isEqualTo(actionCount);
+    assertThat(buildGraphMetrics)
+        .comparingExpectedFieldsOnly()
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                .setOutputFileConfiguredTargetCount(1)
+                .setOtherConfiguredTargetCount(1)
+                .build());
+    int outputArtifactCount = buildGraphMetrics.getOutputArtifactCount();
+    assertThat(outputArtifactCount).isGreaterThan(0);
+    int graphSize = buildGraphMetrics.getPostInvocationSkyframeNodeCount();
+    assertThat(graphSize).isGreaterThan(0);
+    ArtifactMetrics artifactMetrics =
+        buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics();
+    assertThat(artifactMetrics.getSourceArtifactsRead().getSizeInBytes()).isGreaterThan(0L);
+    assertThat(artifactMetrics.getOutputArtifactsSeen())
+        .isEqualTo(ArtifactMetrics.FilesMetric.newBuilder().setSizeInBytes(4L).setCount(3).build());
+    assertThat(artifactMetrics.getOutputArtifactsFromActionCache().getCount()).isEqualTo(0);
+    assertThat(artifactMetrics.getTopLevelArtifacts())
+        .isEqualTo(ArtifactMetrics.FilesMetric.newBuilder().setSizeInBytes(4L).setCount(1).build());
+    // Adjust for the "alias", "input" and "output" configured targets, which won't be in play
+    // later.
+    actionLookupValueCount -= 3;
+
+    // Now do a build of a target with non-trivial transitive deps, and verify the metrics. Blaze
+    // won't redo analysis of dependencies or re-read their sources.
+    buildTarget("//a");
+
+    buildGraphMetrics = buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics();
+    assertThat(buildGraphMetrics)
+        .comparingExpectedFieldsOnly()
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                // Two dependencies and three source files for action lookup values.
+                .setActionLookupValueCount(5 + actionLookupValueCount)
+                .setActionCount(2 + actionCount)
+                .setInputFileConfiguredTargetCount(4)
+                .setOutputArtifactCount(2 + outputArtifactCount)
+                .build());
+
+    int newGraphSize = buildGraphMetrics.getPostInvocationSkyframeNodeCount();
+    assertThat(newGraphSize).isGreaterThan(graphSize);
+
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            ArtifactMetrics.newBuilder()
+                // 2 distinct artifacts of 6 and 3 bytes, with a symlink to the 3-byte one.
+                .setSourceArtifactsRead(
+                    ArtifactMetrics.FilesMetric.newBuilder().setSizeInBytes(12).setCount(3))
+                // b outputs 9 bytes, c outputs 6, a outputs 15, 30 total.
+                .setOutputArtifactsSeen(
+                    ArtifactMetrics.FilesMetric.newBuilder().setSizeInBytes(30).setCount(3).build())
+                .setTopLevelArtifacts(
+                    ArtifactMetrics.FilesMetric.newBuilder().setSizeInBytes(15).setCount(1).build())
+                .build());
+
+    // Do a null build. No useful analysis stats.
+    buildTarget("//a");
+
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                .setPostInvocationSkyframeNodeCount(newGraphSize)
+                .build());
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(ArtifactMetrics.getDefaultInstance());
+
+    // Change a BUILD file and rebuild: no source artifacts read, but analysis stats present.
+    write(
+        "a/BUILD",
+        "genrule(name = 'a', srcs = ['//b:c', '//b:b'], outs = ['a.out'], cmd = 'cat $(SRCS) >"
+            + " $@')");
+    buildTarget("//a");
+
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics())
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                .setActionLookupValueCount(5 + actionLookupValueCount)
+                .setActionLookupValueCountNotIncludingAspects(5 + actionLookupValueCount)
+                .setActionCount(2 + actionCount)
+                .setActionCountNotIncludingAspects(2 + actionCount)
+                .setInputFileConfiguredTargetCount(4)
+                .setOutputArtifactCount(2 + outputArtifactCount)
+                // ArtifactNestedSet node for stale nested set is still in graph, since it is
+                // technically still valid (even though nobody wants that nested set anymore).
+                .setPostInvocationSkyframeNodeCount(newGraphSize + 1)
+                .build());
+    ArtifactMetrics.FilesMetric singleFileMetric =
+        ArtifactMetrics.FilesMetric.newBuilder().setSizeInBytes(15L).setCount(1).build();
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            ArtifactMetrics.newBuilder()
+                .setOutputArtifactsSeen(singleFileMetric)
+                .setTopLevelArtifacts(singleFileMetric)
+                .build());
+
+    // Change BUILD file back, but don't do a full build.
+    write(
+        "a/BUILD",
+        "genrule(name = 'a', srcs = ['//b:c', '//b:b'], outs = ['a.out'], cmd = 'cat $(SRCS) >"
+            + " $@')");
+    addOptions("--nobuild");
+    buildTarget("//a");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics())
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                .setActionLookupValueCount(5 + actionLookupValueCount)
+                .setActionLookupValueCountNotIncludingAspects(5 + actionLookupValueCount)
+                .setActionCount(2 + actionCount)
+                .setActionCountNotIncludingAspects(2 + actionCount)
+                .setInputFileConfiguredTargetCount(4)
+                .setOutputArtifactCount(2 + outputArtifactCount)
+                .setPostInvocationSkyframeNodeCount(newGraphSize + 1)
+                .build());
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(ArtifactMetrics.getDefaultInstance());
+
+    // Null --nobuild.
+    buildTarget("//a");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                // Stale action execution nodes have been GC'ed.
+                .setPostInvocationSkyframeNodeCount(newGraphSize - 1)
+                .build());
+
+    // Do a null full build. Back to baseline.
+    addOptions("--build");
+    buildTarget("//a");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                // We now have three copies of the ArtifactNestedSetKey, since the re-analysis
+                // didn't re-use the old nested set.
+                .setPostInvocationSkyframeNodeCount(newGraphSize + 2)
+                .build());
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            ArtifactMetrics.newBuilder()
+                .setOutputArtifactsSeen(singleFileMetric)
+                .setOutputArtifactsFromActionCache(singleFileMetric)
+                .setTopLevelArtifacts(singleFileMetric)
+                .build());
+
+    // Change a source file. It and its symlink are both re-read.
+    write("b/c.in", "1234");
+    buildTarget("//a");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getBuildGraphMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            BuildGraphMetrics.newBuilder()
+                // Analysis not re-triggered, even of the input file that was changed.
+                .setInputFileConfiguredTargetCount(0)
+                .setPostInvocationSkyframeNodeCount(newGraphSize + 2)
+                .build());
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            ArtifactMetrics.newBuilder()
+                .setSourceArtifactsRead(
+                    ArtifactMetrics.FilesMetric.newBuilder()
+                        .setSizeInBytes(OS.getCurrent() == OS.WINDOWS ? 6 : 10)
+                        .setCount(OS.getCurrent() == OS.WINDOWS ? 1 : 2))
+                .setOutputArtifactsSeen(
+                    ArtifactMetrics.FilesMetric.newBuilder()
+                        .setSizeInBytes(42L)
+                        .setCount(3)
+                        .build())
+                .setTopLevelArtifacts(
+                    ArtifactMetrics.FilesMetric.newBuilder()
+                        .setSizeInBytes(21L)
+                        .setCount(1)
+                        .build())
+                .build());
+  }
+
+  @Test
+  public void treeArtifactAndTopLevelMetrics() throws Exception {
+    write(
+        "foo/tree_artifact_rule.bzl",
+        "def _tree_artifact_files_impl(ctx):",
+        "    directory = ctx.actions.declare_directory(ctx.attr.name + '_artifact')",
+        "    ctx.actions.run_shell(",
+        "      outputs = [directory],",
+        "      command = 'cd %s && echo a > file1 && echo bcde > file2}' % (directory.path))",
+        "    return [DefaultInfo(files = depset([directory]))]",
+        "def _several_outputs_impl(ctx):",
+        "    file = ctx.actions.declare_file(ctx.attr.name + '_file')",
+        "    ctx.actions.write(output = file, content = 'abc')",
+        "    return [DefaultInfo(files = depset([file])),",
+        "            OutputGroupInfo(dep_files = ctx.attr.dep[DefaultInfo].files)]",
+        "my_tree = rule(implementation = _tree_artifact_files_impl)",
+        "my_rule = rule(",
+        "               implementation = _several_outputs_impl,",
+        "               attrs = { 'dep': attr.label()},",
+        " )");
+    write(
+        "foo/BUILD",
+        "load('//foo:tree_artifact_rule.bzl', 'my_rule', 'my_tree')",
+        "my_tree(name = 'tree')",
+        "my_rule(name = 'top', dep = ':tree')");
+    // Null build to populate silly things like fake build-info artifact.
+    buildTarget();
+    addOptions("--output_groups=+dep_files");
+    buildTarget("//foo:top");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getArtifactMetrics())
+        .ignoringFieldAbsence()
+        .isEqualTo(
+            ArtifactMetrics.newBuilder()
+                .setOutputArtifactsSeen(
+                    ArtifactMetrics.FilesMetric.newBuilder()
+                        .setSizeInBytes(10L)
+                        .setCount(3)
+                        .build())
+                .setTopLevelArtifacts(
+                    ArtifactMetrics.FilesMetric.newBuilder()
+                        .setSizeInBytes(10L)
+                        .setCount(3)
+                        .build())
+                .build());
+  }
+
+  @Test
+  public void testTargetCounts() throws Exception {
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getTargetMetrics().getTargetsConfigured()).isGreaterThan(0L);
+    assertThat(buildMetrics.getTargetMetrics().getTargetsConfiguredNotIncludingAspects())
+        .isGreaterThan(0L);
+  }
+
+  @Test
+  public void testTargetsCountsAreZeroOnSecondBuild() throws Exception {
+    buildTarget("//foo:foo");
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getTargetMetrics().getTargetsLoaded()).isEqualTo(0);
+    assertThat(buildMetrics.getTargetMetrics().getTargetsConfigured()).isEqualTo(0);
+  }
+
+  @Test
+  public void aspectLoadedMetric() throws Exception {
+    write(
+        "foo/foo.bzl",
+        "def _aspect_impl(target, ctx):",
+        "  outfile = ctx.actions.declare_file(ctx.rule.attr.name + 'aspect.out')",
+        "  ctx.actions.run_shell(",
+        "    outputs = [outfile],",
+        "    command = 'echo \"1\" > ' + outfile.path,",
+        "  )",
+        "  return [OutputGroupInfo(files = [outfile])]",
+        "",
+        "def _impl(ctx):",
+        "    return []",
+        "",
+        "rule_aspect = aspect(implementation = _aspect_impl, attr_aspects = ['deps'])",
+        "",
+        "aspected = rule(",
+        "    implementation = _impl,",
+        "    attrs = { 'deps': attr.label_list(aspects = [rule_aspect]) })");
+    write(
+        "foo/BUILD",
+        "load('//foo:foo.bzl', 'aspected')",
+        "aspected(name = 'top', deps = [':dep'])",
+        "aspected(name = 'dep')");
+    buildTarget("//foo:top");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    // 2 additional: aspect and workspace status action.
+    assertThat(buildMetrics.getTargetMetrics().getTargetsConfigured())
+        .isEqualTo(2L + buildMetrics.getTargetMetrics().getTargetsConfiguredNotIncludingAspects());
+    assertThat(buildMetrics.getActionSummary().getActionsCreated())
+        .isEqualTo(2L + buildMetrics.getActionSummary().getActionsCreatedNotIncludingAspects());
+    // Traversing the Skyframe graph doesn't hit the workspace status action.
+    assertThat(buildMetrics.getBuildGraphMetrics().getActionLookupValueCount())
+        .isEqualTo(
+            1L
+                + buildMetrics
+                    .getBuildGraphMetrics()
+                    .getActionLookupValueCountNotIncludingAspects());
+    assertThat(buildMetrics.getBuildGraphMetrics().getActionCount())
+        .isEqualTo(1L + buildMetrics.getBuildGraphMetrics().getActionCountNotIncludingAspects());
+
+    // Analyzing a new target makes the aspect drop out of the target metric, but the build graph
+    // metric still knows about it.
+    write("bar/BUILD", "genrule(name = 'bar', outs = ['out'], cmd = 'touch $@')");
+    buildTarget("//foo:top", "//bar:bar");
+    buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getTargetMetrics().getTargetsConfigured())
+        .isEqualTo(buildMetrics.getTargetMetrics().getTargetsConfiguredNotIncludingAspects());
+    assertThat(buildMetrics.getActionSummary().getActionsCreated())
+        .isEqualTo(buildMetrics.getActionSummary().getActionsCreatedNotIncludingAspects());
+    assertThat(buildMetrics.getBuildGraphMetrics().getActionLookupValueCount())
+        .isEqualTo(
+            1L
+                + buildMetrics
+                    .getBuildGraphMetrics()
+                    .getActionLookupValueCountNotIncludingAspects());
+    assertThat(buildMetrics.getBuildGraphMetrics().getActionCount())
+        .isEqualTo(1L + buildMetrics.getBuildGraphMetrics().getActionCountNotIncludingAspects());
+  }
+
+  @Test
+  public void testPackagesLoaded() throws Exception {
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getPackageMetrics().getPackagesLoaded()).isGreaterThan(0L);
+  }
+
+  @Test
+  public void testPackagesLoadedIsZeroOnSecondBuild() throws Exception {
+    buildTarget("//foo:foo");
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getPackageMetrics().getPackagesLoaded()).isEqualTo(0);
+  }
+
+  @Test
+  public void testAnalysisTimeInMs() throws Exception {
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getTimingMetrics().getAnalysisPhaseTimeInMs()).isGreaterThan(0);
+  }
+
+  @Test
+  public void testUsedHeapSizePostBuild() throws Exception {
+    // TODO(bazel-team): Fix recording used heap size on Windows.
+    Assume.assumeTrue(OS.getCurrent() != OS.WINDOWS);
+    addOptions("--memory_profile=/dev/null");
+
+    // The options from above do not get added to the initial command environment,
+    // so it has to be recreated here.
+    runtimeWrapper.initializeOptionsParser();
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getMemoryMetrics().getUsedHeapSizePostBuild()).isGreaterThan(0L);
+    // Note that we cannot test peak heap size here since the tiny builds that we do here don't
+    // trigger a full GC.
+  }
+
+  @Test
+  public void testUsedHeapSizePostBuildCollectionOffByDefault() throws Exception {
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getMemoryMetrics().getUsedHeapSizePostBuild()).isEqualTo(0);
+    assertThat(buildMetrics.getMemoryMetrics().getPeakPostGcHeapSize()).isEqualTo(0);
+  }
+
+  @Test
+  public void testWallTimePostBuild() throws Exception {
+    buildTarget("//foo:foo");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    assertThat(buildMetrics.getTimingMetrics().getWallTimeInMs()).isGreaterThan(0);
+  }
+
+  @Test
+  public void cumulativeMetrics() throws Exception {
+    buildTarget("//foo:foo");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getCumulativeMetrics())
+        .isEqualTo(CumulativeMetrics.newBuilder().setNumAnalyses(1).setNumBuilds(1).build());
+
+    addOptions("--nobuild");
+    buildTarget("//foo:foo");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getCumulativeMetrics())
+        .isEqualTo(CumulativeMetrics.newBuilder().setNumAnalyses(2).setNumBuilds(1).build());
+
+    addOptions("--build", "--noanalyze");
+    buildTarget("//foo:foo");
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getCumulativeMetrics())
+        .isEqualTo(CumulativeMetrics.newBuilder().setNumAnalyses(2).setNumBuilds(1).build());
+
+    write(
+        "foo/BUILD",
+        "genrule(",
+        "    name = 'foo',",
+        "    outs = ['dir'],",
+        "    srcs = ['//noexist:noexist'],",
+        "    cmd = '/bin/mkdir $(location dir)',",
+        ")");
+
+    addOptions("--analyze");
+    assertThrows(ViewCreationFailedException.class, () -> buildTarget("//foo:foo"));
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getCumulativeMetrics())
+        .isEqualTo(CumulativeMetrics.newBuilder().setNumAnalyses(3).setNumBuilds(1).build());
+
+    write(
+        "foo/BUILD",
+        "genrule(",
+        "    name = 'foo',",
+        "    outs = ['dir'],",
+        "    cmd = '/bin/false',",
+        ")");
+
+    assertThrows(BuildFailedException.class, () -> buildTarget("//foo:foo"));
+    assertThat(buildMetricsEventListener.event.getBuildMetrics().getCumulativeMetrics())
+        .isEqualTo(CumulativeMetrics.newBuilder().setNumAnalyses(4).setNumBuilds(2).build());
+  }
+
+  @Test
+  public void testActionData() throws Exception {
+    write(
+        "bar/BUILD",
+        "genrule(name='bar', srcs=[':dep1',':dep2'], outs=['out'], cmd='touch $@')",
+        "genrule(name='dep1', outs=['out1'], cmd='touch $@')",
+        "genrule(name='dep2', outs=['out2'], cmd='touch $@')");
+    buildTarget("//bar");
+    BuildMetrics buildMetrics = buildMetricsEventListener.event.getBuildMetrics();
+    List<ActionData> actionDataList = buildMetrics.getActionSummary().getActionDataList();
+    assertThat(actionDataList).hasSize(2);
+    assertThat(actionDataList.get(0).getMnemonic()).isEqualTo("Genrule");
+    assertThat(actionDataList.get(0).getActionsExecuted()).isEqualTo(3);
+
+    assertThat(actionDataList.get(1).getMnemonic()).isEqualTo("DummyBuildInfoAction");
+    assertThat(actionDataList.get(1).getActionsExecuted()).isEqualTo(1);
+
+    for (ActionData actionData : actionDataList) {
+      assertThat(actionData.getFirstStartedMs()).isAtMost(actionData.getLastEndedMs());
+    }
+  }
+}