Support setting different UI progress modes

This change adds a flag to select between UI progress modes. The initial
values are oldest_actions and mnemonic_histogram; the latter of these is
also added in this change.

The new mode turned out to be useful for debugging an issue where Bazel
ran out of memory due to a high number of ActionExecutionFunction
Skyframe restarts.

PiperOrigin-RevId: 255952215
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandEventHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandEventHandler.java
index 9a4bae2..06a3d50 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandEventHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/BlazeCommandEventHandler.java
@@ -19,6 +19,7 @@
 import com.google.devtools.build.lib.events.EventKind;
 import com.google.devtools.build.lib.events.Location;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
+import com.google.devtools.build.lib.runtime.ExperimentalStateTracker.ProgressMode;
 import com.google.devtools.build.lib.util.io.OutErr;
 import com.google.devtools.build.lib.vfs.PathFragment;
 import com.google.devtools.common.options.EnumConverter;
@@ -62,6 +63,13 @@
     }
   }
 
+  /** Progress mode converter. */
+  public static class ProgressModeConverter extends EnumConverter<ProgressMode> {
+    public ProgressModeConverter() {
+      super(ProgressMode.class, "--experimental_ui_mode setting");
+    }
+  }
+
   public static class Options extends OptionsBase {
 
     @Option(
@@ -223,17 +231,30 @@
     public boolean experimentalUiDebugAllEvents;
 
     @Option(
+        name = "experimental_ui_mode",
+        defaultValue = "oldest_actions",
+        converter = ProgressModeConverter.class,
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
+        help =
+            "Determines what kind of data is shown in the detailed progress bar. By default, it is "
+                + "set to show the oldest actions and their running time. The underlying data "
+                + "source is usually sampled in a mode-dependend way to fit within the number of "
+                + "lines given by --ui_actions_shown.")
+    public ProgressMode uiProgressMode;
+
+    @Option(
         name = "ui_actions_shown",
         oldName = "experimental_ui_actions_shown",
         defaultValue = "8",
         documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-        effectTags = {OptionEffectTag.UNKNOWN},
+        effectTags = {OptionEffectTag.TERMINAL_OUTPUT},
         help =
             "Number of concurrent actions shown in the detailed progress bar; each "
                 + "action is shown on a separate line. The progress bar always shows "
                 + "at least one one, all numbers less than 1 are mapped to 1. "
                 + "This option has no effect if --noui is set.")
-    public int experimentalUiActionsShown;
+    public int uiSamplesShown;
 
     @Option(
         name = "experimental_ui_limit_console_output",
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalEventHandler.java b/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalEventHandler.java
index 9a048be..e6ffefe 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalEventHandler.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalEventHandler.java
@@ -270,7 +270,7 @@
         this.cursorControl
             ? new ExperimentalStateTracker(clock, this.terminalWidth - 2)
             : new ExperimentalStateTracker(clock);
-    this.stateTracker.setSampleSize(options.experimentalUiActionsShown);
+    this.stateTracker.setProgressMode(options.uiProgressMode, options.uiSamplesShown);
     this.numLinesProgressBar = 0;
     if (this.cursorControl) {
       this.minimalDelayMillis = Math.round(options.showProgressRateLimit * 1000);
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalStateTracker.java b/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalStateTracker.java
index 32e572e..8d56640 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalStateTracker.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/ExperimentalStateTracker.java
@@ -18,7 +18,10 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Comparators;
+import com.google.common.collect.HashMultiset;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Multiset;
 import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.ActionCompletionEvent;
@@ -57,6 +60,7 @@
 import java.util.Comparator;
 import java.util.Deque;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
@@ -69,6 +73,10 @@
  * An experimental state tracker for the new experimental UI.
  */
 class ExperimentalStateTracker {
+  enum ProgressMode {
+    OLDEST_ACTIONS,
+    MNEMONIC_HISTOGRAM
+  }
 
   static final long SHOW_TIME_THRESHOLD_SECONDS = 3;
   static final String ELLIPSIS = "...";
@@ -80,6 +88,7 @@
   static final int NANOS_PER_SECOND = 1000000000;
   static final String URL_PROTOCOL_SEP = "://";
 
+  private ProgressMode progressMode = ProgressMode.OLDEST_ACTIONS;
   private int sampleSize = 3;
 
   private String status;
@@ -342,15 +351,10 @@
     this(clock, 0);
   }
 
-  /**
-   * Set the maximal number of actions shown in the progress bar.
-   */
-  void setSampleSize(int sampleSize) {
-    if (sampleSize >= 1) {
-      this.sampleSize = sampleSize;
-    } else {
-      this.sampleSize = 1;
-    }
+  /** Set the progress bar mode and sample size. */
+  void setProgressMode(ProgressMode progressMode, int sampleSize) {
+    this.progressMode = progressMode;
+    this.sampleSize = Math.max(1, sampleSize);
   }
 
   void buildStarted(BuildStartingEvent event) {
@@ -768,6 +772,31 @@
     }
   }
 
+  private void printActionState(AnsiTerminalWriter terminalWriter) throws IOException {
+    switch (progressMode) {
+      case OLDEST_ACTIONS:
+        sampleOldestActions(terminalWriter);
+        break;
+      case MNEMONIC_HISTOGRAM:
+        showMnemonicHistogram(terminalWriter);
+        break;
+    }
+  }
+
+  private void showMnemonicHistogram(AnsiTerminalWriter terminalWriter) throws IOException {
+    Multiset<String> mnemonicHistogram = HashMultiset.create();
+    for (Map.Entry<Artifact, ActionState> action : activeActions.entrySet()) {
+      mnemonicHistogram.add(action.getValue().action.getMnemonic());
+    }
+    List<Multiset.Entry<String>> sorted =
+        mnemonicHistogram.entrySet().stream()
+            .collect(
+                Comparators.greatest(sampleSize, Comparator.comparingLong((e) -> e.getCount())));
+    for (Multiset.Entry<String> entry : sorted) {
+      terminalWriter.newline().append("    " + entry.getElement() + " " + entry.getCount());
+    }
+  }
+
   private void sampleOldestActions(AnsiTerminalWriter terminalWriter) throws IOException {
     int count = 0;
     int totalCount = 0;
@@ -1109,7 +1138,7 @@
         terminalWriter.normal().append(" " + statusMessage);
         maybeShowRecentTest(
             terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
-        sampleOldestActions(terminalWriter);
+        printActionState(terminalWriter);
       }
     }
     if (!shortVersion) {
diff --git a/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java b/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java
index dac992e..4176e9b 100644
--- a/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/runtime/ExperimentalStateTrackerTest.java
@@ -46,6 +46,7 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.events.ExtendedEventHandler.FetchProgress;
 import com.google.devtools.build.lib.packages.AspectDescriptor;
+import com.google.devtools.build.lib.runtime.ExperimentalStateTracker.ProgressMode;
 import com.google.devtools.build.lib.runtime.ExperimentalStateTracker.StrategyIds;
 import com.google.devtools.build.lib.skyframe.LoadingPhaseStartedEvent;
 import com.google.devtools.build.lib.skyframe.PackageProgressReceiver;
@@ -60,6 +61,7 @@
 import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
 import java.io.IOException;
 import java.net.URL;
+import java.time.Duration;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -329,7 +331,7 @@
 
     // For various sample sizes verify the progress bar
     for (int i = 1; i < 11; i++) {
-      stateTracker.setSampleSize(i);
+      stateTracker.setProgressMode(ProgressMode.OLDEST_ACTIONS, i);
       LoggingTerminalWriter terminalWriter = new LoggingTerminalWriter(/*discardHighlight=*/ true);
       stateTracker.writeProgressBar(terminalWriter);
       String output = terminalWriter.getTranscript();
@@ -1248,6 +1250,43 @@
     assertThat(output, containsString("30 fetches"));
   }
 
+  private Action mockActionWithMnemonic(String mnemonic, String primaryOutput) {
+    Path path = outputBase.getRelative(PathFragment.create(primaryOutput));
+    Artifact artifact =
+        ActionsTestUtil.createArtifact(ArtifactRoot.asSourceRoot(Root.fromPath(outputBase)), path);
+    Action action = Mockito.mock(Action.class);
+    when(action.getMnemonic()).thenReturn(mnemonic);
+    when(action.getPrimaryOutput()).thenReturn(artifact);
+    return action;
+  }
+
+  @Test
+  public void testMnemonicHistogram() throws IOException {
+    // Verify that the number of actions shown in the progress bar can be set as sample size.
+    ManualClock clock = new ManualClock();
+    clock.advanceMillis(Duration.ofSeconds(123).toMillis());
+    ExperimentalStateTracker stateTracker = new ExperimentalStateTracker(clock);
+    clock.advanceMillis(Duration.ofSeconds(2).toMillis());
+
+    // Start actions with 10 different mnemonics Mnemonic0-9, n+1 of each mnemonic.
+    for (int i = 0; i < 10; i++) {
+      clock.advanceMillis(Duration.ofSeconds(1).toMillis());
+      for (int j = 0; j <= i; j++) {
+        Action action = mockActionWithMnemonic("Mnemonic" + i, "action-" + i + "-" + j + ".out");
+        stateTracker.actionStarted(new ActionStartedEvent(action, clock.nanoTime()));
+      }
+    }
+
+    for (int sampleSize = 1; sampleSize < 11; sampleSize++) {
+      stateTracker.setProgressMode(ProgressMode.MNEMONIC_HISTOGRAM, sampleSize);
+      LoggingTerminalWriter terminalWriter = new LoggingTerminalWriter(/*discardHighlight=*/ true);
+      stateTracker.writeProgressBar(terminalWriter);
+      String output = terminalWriter.getTranscript();
+      assertThat(output).contains("Mnemonic" + (10 - sampleSize) + " " + (10 - sampleSize + 1));
+      assertThat(output).doesNotContain("Mnemonic" + (10 - sampleSize - 1));
+    }
+  }
+
   private static class FetchEvent implements FetchProgress {
     private final String id;