Use lock-free synchronization in the fast paths of ExperimentalEventHandler.
Experiments have shown that these can cause a substantial amount of contention.

RELNOTES: None.
PiperOrigin-RevId: 208886720
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 16caed0..e3cf8f8 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
@@ -14,6 +14,7 @@
 package com.google.devtools.build.lib.runtime;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.eventbus.AllowConcurrentEvents;
 import com.google.common.eventbus.Subscribe;
 import com.google.common.primitives.Bytes;
 import com.google.common.util.concurrent.Uninterruptibles;
@@ -632,18 +633,21 @@
   }
 
   @Subscribe
+  @AllowConcurrentEvents
   public void actionStarted(ActionStartedEvent event) {
     stateTracker.actionStarted(event);
     refresh();
   }
 
   @Subscribe
+  @AllowConcurrentEvents
   public void actionStatusMessage(ActionStatusMessage event) {
     stateTracker.actionStatusMessage(event);
     refresh();
   }
 
   @Subscribe
+  @AllowConcurrentEvents
   public void actionCompletion(ActionCompletionEvent event) {
     stateTracker.actionCompletion(event);
     refreshSoon();
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 0b943d4..7ed6aab 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
@@ -15,7 +15,9 @@
 
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import com.google.devtools.build.lib.actions.Action;
 import com.google.devtools.build.lib.actions.ActionCompletionEvent;
 import com.google.devtools.build.lib.actions.ActionStartedEvent;
@@ -42,14 +44,16 @@
 import com.google.devtools.build.lib.view.test.TestStatus.BlazeTestStatus;
 import java.io.IOException;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Deque;
 import java.util.HashSet;
-import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
-import java.util.stream.Stream;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 /**
  * An experimental state tracker for the new experimental UI.
@@ -77,21 +81,24 @@
   // Non-positive values indicate not to aim for a particular width.
   private final int targetWidth;
 
-  // currently running actions, using the path of the primary
-  // output as unique identifier.
-  private final LinkedHashSet<String> nonExecutingActions;
-  private final LinkedHashSet<String> executingActions;
-  private final Map<String, Action> actions;
-  private final Map<String, String> actionStatus;
-  // Time the action entered its current status.
-  private final Map<String, Long> actionNanoStartTimes;
-  // As sometimes the executing stategy might be sent before the action started,
-  // we have to keep track of a small number of executing, but not yet started
-  // actions.
-  private final Set<String> notStartedExecutingActions;
+  private static class ActionState {
+    public final Action action;
+    public final long nanoStartTime;
+    public final boolean executing;
+    public final String status;
 
-  // running downloads are identified by the original URL they were trying to
-  // access.
+    private ActionState(Action action, long nanoStartTime, boolean executing, String status) {
+      this.action = action;
+      this.nanoStartTime = nanoStartTime;
+      this.executing = executing;
+      this.status = status;
+    }
+  }
+
+  private final Map<String, ActionState> activeActions;
+  private final Map<String, String> notStartedActionStatus;
+
+  // running downloads are identified by the original URL they were trying to access.
   private final Deque<String> runningDownloads;
   private final Map<String, Long> downloadNanoStartTimes;
   private final Map<String, FetchProgress> downloads;
@@ -102,7 +109,7 @@
   // was discovered as a test.
   private final Map<Label, Set<String>> testActions;
 
-  private int actionsCompleted;
+  private final AtomicInteger actionsCompleted;
   private int totalTests;
   private int completedTests;
   private TestSummary mostRecentTest;
@@ -119,19 +126,17 @@
   private long bepTransportClosingStartTimeMillis;
 
   ExperimentalStateTracker(Clock clock, int targetWidth) {
-    this.nonExecutingActions = new LinkedHashSet<>();
-    this.executingActions = new LinkedHashSet<>();
-    this.actions = new TreeMap<>();
-    this.actionNanoStartTimes = new TreeMap<>();
-    this.actionStatus = new TreeMap<>();
-    this.testActions = new TreeMap<>();
+    this.activeActions = new ConcurrentHashMap<>();
+    this.notStartedActionStatus = new ConcurrentHashMap<>();
+
+    this.actionsCompleted = new AtomicInteger();
+    this.testActions = new ConcurrentHashMap<>();
     this.runningDownloads = new ArrayDeque<>();
     this.downloads = new TreeMap<>();
     this.downloadNanoStartTimes = new TreeMap<>();
     this.ok = true;
     this.clock = clock;
     this.targetWidth = targetWidth;
-    this.notStartedExecutingActions = new TreeSet<>();
   }
 
   ExperimentalStateTracker(Clock clock) {
@@ -169,10 +174,6 @@
     }
   }
 
-  synchronized int totalNumberOfActions() {
-    return nonExecutingActions.size() + executingActions.size();
-  }
-
   /**
    * Make the state tracker aware of the fact that the analyis has finished. Return a summary of the
    * work done in the analysis phase.
@@ -200,6 +201,7 @@
 
     if (event.getResult().getSuccess()) {
       status = "INFO";
+      int actionsCompleted = this.actionsCompleted.get();
       if (failedTests == 0) {
         additionalMessage =
             additionalInfo + "Build completed successfully, "
@@ -240,21 +242,17 @@
     }
   }
 
-  synchronized void actionStarted(ActionStartedEvent event) {
+  void actionStarted(ActionStartedEvent event) {
     Action action = event.getAction();
     String name = action.getPrimaryOutput().getPath().getPathString();
     Long nanoStartTime = event.getNanoTimeStart();
 
-    // We might already know about the action, if the status message was sent over the
-    // bus before the start notification. In this case the action is already executing,
-    // otherwise not yet.
-    if (notStartedExecutingActions.remove(name)) {
-      executingActions.add(name);
-    } else {
-      nonExecutingActions.add(name);
-    }
-    actions.put(name, action);
-    actionNanoStartTimes.put(name, nanoStartTime);
+    String status = notStartedActionStatus.remove(name);
+    boolean nowExecuting = status != null;
+    activeActions.put(
+        name,
+        new ActionState(action, nanoStartTime, nowExecuting, nowExecuting ? status : null));
+
     if (action.getOwner() != null) {
       Label owner = action.getOwner().getLabel();
       if (owner != null) {
@@ -266,41 +264,39 @@
     }
   }
 
-  synchronized void actionStatusMessage(ActionStatusMessage event) {
+  void actionStatusMessage(ActionStatusMessage event) {
     String strategy = event.getStrategy();
     String name = event.getActionMetadata().getPrimaryOutput().getPath().getPathString();
-    executingActions.remove(name);
-    nonExecutingActions.remove(name);
 
-    actionNanoStartTimes.put(name, clock.nanoTime());
+    ActionState state = activeActions.remove(name);
+
     if (strategy != null) {
-      actionStatus.put(name, strategy);
-      // only add the action, if we already know about it being started
-      if (actions.get(name) != null) {
-        executingActions.add(name);
+      if (state != null) {
+        activeActions.put(
+            name, new ActionState(state.action, clock.nanoTime(), /*executing=*/ true, strategy));
       } else {
-        notStartedExecutingActions.add(name);
+        notStartedActionStatus.put(name, strategy);
       }
     } else {
-      String message = event.getMessage();
-      actionStatus.put(name, message);
-      // only add the action, if we already know about it being started
-      if (actions.get(name) != null) {
-        nonExecutingActions.add(name);
+      if (state != null) {
+        activeActions.put(
+            name,
+            new ActionState(
+                state.action, clock.nanoTime(), /*executing=*/ false, event.getMessage()));
       }
-      notStartedExecutingActions.remove(name);
+      notStartedActionStatus.remove(name);
     }
   }
 
-  synchronized void actionCompletion(ActionCompletionEvent event) {
-    actionsCompleted++;
+  void actionCompletion(ActionCompletionEvent event) {
+    actionsCompleted.incrementAndGet();
     Action action = event.getAction();
     String name = action.getPrimaryOutput().getPath().getPathString();
-    executingActions.remove(name);
-    nonExecutingActions.remove(name);
-    actions.remove(name);
-    actionNanoStartTimes.remove(name);
-    actionStatus.remove(name);
+    activeActions.remove(name);
+    Preconditions.checkState(
+        !notStartedActionStatus.containsKey(name),
+        "Should not complete an action before starting it: %s",
+        name);
 
     if (action.getOwner() != null) {
       Label owner = action.getOwner().getLabel();
@@ -390,30 +386,33 @@
     }
 
     String sep = "";
-    int count = 0;
+    boolean allReported = true;
     for (String action : allActions) {
-      long nanoRuntime = nanoTime - actionNanoStartTimes.get(action);
+      ActionState actionState = activeActions.get(action);
+      if (actionState == null) {
+        // This action must have completed while we were constructing this output. Skip it.
+        continue;
+      }
+      long nanoRuntime = nanoTime - actionState.nanoStartTime;
       long runtimeSeconds = nanoRuntime / NANOS_PER_SECOND;
       String text = sep + runtimeSeconds + "s";
       if (remainingWidth < text.length()) {
+        allReported = false;
         break;
       }
       message.append(text);
       remainingWidth -= text.length();
-      count++;
       sep = ", ";
     }
-    if (count == allActions.size()) {
-      postfix = "]";
-    }
-    return message.append(postfix).toString();
+    return message.append(allReported ? "]" : postfix).toString();
   }
 
   // Describe an action by a string of the desired length; if describing that action includes
   // describing other actions, add those to the to set of actions to skip in further samples of
   // actions.
-  private String describeAction(String name, long nanoTime, int desiredWidth, Set<String> toSkip) {
-    Action action = actions.get(name);
+  private String describeAction(
+      ActionState actionState, long nanoTime, int desiredWidth, Set<String> toSkip) {
+    Action action = actionState.action;
     if (action.getOwner() != null) {
       Label owner = action.getOwner().getLabel();
       if (owner != null) {
@@ -429,13 +428,13 @@
 
     String postfix = "";
     String prefix = "";
-    long nanoRuntime = nanoTime - actionNanoStartTimes.get(name);
+    long nanoRuntime = nanoTime - actionState.nanoStartTime;
     long runtimeSeconds = nanoRuntime / NANOS_PER_SECOND;
     String strategy = null;
-    if (executingActions.contains(name)) {
-      strategy = actionStatus.get(name);
+    if (actionState.executing) {
+      strategy = actionState.status;
     } else {
-      String status = actionStatus.get(name);
+      String status = actionState.status;
       if (status == null) {
         status = NO_STATUS;
       }
@@ -508,23 +507,37 @@
     return prefix + message + postfix;
   }
 
-  /**
-   * Stream of actions in decreasing order of importance for the UI. I.e., first have all executing
-   * actions and then all non-executing actions, each time in order of increasing start time for
-   * that state.
-   */
-  private Stream<String> sortedActions() {
-    return Stream.concat(executingActions.stream(), nonExecutingActions.stream());
+  private ActionState getOldestAction() {
+    long minStart = Long.MAX_VALUE;
+    ActionState result = null;
+    for (ActionState action : activeActions.values()) {
+      if (action.nanoStartTime < minStart) {
+        minStart = action.nanoStartTime;
+        result = action;
+      }
+    }
+    return result;
   }
 
-  private synchronized String countActions() {
-    int actionsCount = totalNumberOfActions();
+  private String countActions() {
+    // TODO(djasper): Iterating over the actions here is slow, but it's only done once per refresh
+    // and thus might be faster than trying to update these values in the critical path.
+    // Re-investigate if this ever turns up in a profile.
+    int actionsCount = 0;
+    int executingActionsCount = 0;
+    for (ActionState actionState : activeActions.values()) {
+      actionsCount++;
+      if (actionState.executing) {
+        executingActionsCount++;
+      }
+    }
+
     if (actionsCount == 1) {
-      return " 1 action";
-    } else if (actionsCount == executingActions.size()) {
+      return " 1 action running";
+    } else if (actionsCount == executingActionsCount) {
       return "" + actionsCount + " actions running";
     } else {
-      return "" + actionsCount + " actions, " + executingActions.size() + " running";
+      return "" + actionsCount + " actions, " + executingActionsCount + " running";
     }
   }
 
@@ -532,11 +545,15 @@
     int count = 0;
     int totalCount = 0;
     long nanoTime = clock.nanoTime();
-    int actionCount = totalNumberOfActions();
+    int actionCount = activeActions.size();
     Set<String> toSkip = new TreeSet<>();
-    for (String action : (Iterable<String>) sortedActions()::iterator) {
+    ArrayList<Map.Entry<String, ActionState>> copy = new ArrayList<>(activeActions.entrySet());
+    copy.sort(
+        Comparator.comparing((Map.Entry<String, ActionState> entry) -> !entry.getValue().executing)
+            .thenComparing(entry -> entry.getValue().nanoStartTime));
+    for (Map.Entry<String, ActionState> entry : copy) {
       totalCount++;
-      if (toSkip.contains(action)) {
+      if (toSkip.contains(entry.getKey())) {
         continue;
       }
       count++;
@@ -546,7 +563,9 @@
       }
       int width =
           targetWidth - 4 - ((count >= sampleSize && count < actionCount) ? AND_MORE.length() : 0);
-      terminalWriter.newline().append("    " + describeAction(action, nanoTime, width, toSkip));
+      terminalWriter
+          .newline()
+          .append("    " + describeAction(entry.getValue(), nanoTime, width, toSkip));
     }
     if (totalCount < actionCount) {
       terminalWriter.append(AND_MORE);
@@ -558,7 +577,7 @@
       totalTests = event.getTestTargets().size();
       for (ConfiguredTarget target : event.getTestTargets()) {
         if (target.getLabel() != null) {
-          testActions.put(target.getLabel(), new LinkedHashSet<String>());
+          testActions.put(target.getLabel(), Sets.newConcurrentHashSet());
         }
       }
     }
@@ -602,7 +621,7 @@
     if (status != null) {
       return false;
     }
-    if (totalNumberOfActions() >= 1) {
+    if (activeActions.size() >= 1) {
       return true;
     }
     return false;
@@ -767,7 +786,7 @@
       throws IOException {
     PositionAwareAnsiTerminalWriter terminalWriter =
         new PositionAwareAnsiTerminalWriter(rawTerminalWriter);
-    int actionsCount = totalNumberOfActions();
+    int actionsCount = activeActions.size();
     if (timestamp != null) {
       terminalWriter.append(timestamp);
     }
@@ -814,7 +833,10 @@
       }
       terminalWriter.append(";");
     }
-    if (actionsCount == 0) {
+    // Get the oldest action. Note that actions might have finished in the meantime and thus there
+    // might not be one.
+    ActionState oldestAction = getOldestAction();
+    if (actionsCount == 0 || oldestAction == null) {
       terminalWriter.normal().append(" no action");
       maybeShowRecentTest(terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
     } else if (actionsCount == 1) {
@@ -826,13 +848,12 @@
         maybeShowRecentTest(
             terminalWriter, shortVersion, targetWidth - terminalWriter.getPosition());
         String statusMessage =
-            describeAction(
-                sortedActions().findFirst().get(), clock.nanoTime(), targetWidth - 4, null);
+            describeAction(oldestAction, clock.nanoTime(), targetWidth - 4, null);
         terminalWriter.normal().newline().append("    " + statusMessage);
       } else {
         String statusMessage =
             describeAction(
-                sortedActions().findFirst().get(),
+                oldestAction,
                 clock.nanoTime(),
                 targetWidth - terminalWriter.getPosition() - 1,
                 null);
@@ -842,10 +863,7 @@
       if (shortVersion) {
         String statusMessage =
             describeAction(
-                sortedActions().findFirst().get(),
-                clock.nanoTime(),
-                targetWidth - terminalWriter.getPosition(),
-                null);
+                oldestAction, clock.nanoTime(), targetWidth - terminalWriter.getPosition(), null);
         statusMessage += " ... (" + countActions() + ")";
         terminalWriter.normal().append(" " + statusMessage);
       } else {