Move output artifact pretty-printing to a helper class

Pre-resolve all the convenience symlink to avoid doing a lot of readlink
calls for null builds with a lot of output files.

For an example target with ~15,000 output files, this reduces null build
time by about a third.

PiperOrigin-RevId: 215373507
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/BuildResultPrinter.java b/src/main/java/com/google/devtools/build/lib/buildtool/BuildResultPrinter.java
index 1335887..82879e8 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/BuildResultPrinter.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/BuildResultPrinter.java
@@ -66,6 +66,15 @@
     // problem where the summary message and the exit code disagree.  The logic
     // here is already complex.
 
+    String productName = env.getRuntime().getProductName();
+    PathPrettyPrinter prettyPrinter =
+        OutputDirectoryLinksUtils.getPathPrettyPrinter(
+            request.getBuildOptions().getSymlinkPrefix(productName),
+            productName,
+            env.getWorkspace(),
+            request.getBuildOptions().printWorkspaceInOutputPathsIfNeeded
+                ? env.getWorkingDirectory()
+                : env.getWorkspace());
     OutErr outErr = request.getOutErr();
     Collection<ConfiguredTarget> targetsToPrint = filterTargetsToPrint(configuredTargets);
     Collection<AspectValue> aspectsToPrint = filterAspectsToPrint(aspects);
@@ -100,7 +109,7 @@
               outErr.printErr("Target " + label + " up-to-date:\n");
               headerFlag = false;
             }
-            outErr.printErrLn(formatArtifactForShowResults(artifact, request));
+            outErr.printErrLn(formatArtifactForShowResults(prettyPrinter, artifact));
           }
         }
         if (headerFlag) {
@@ -113,21 +122,10 @@
         // For failed compilation, it is still useful to examine temp artifacts,
         // (ie, preprocessed and assembler files).
         OutputGroupInfo topLevelProvider = OutputGroupInfo.get(target);
-        String productName = env.getRuntime().getProductName();
         if (topLevelProvider != null) {
           for (Artifact temp : topLevelProvider.getOutputGroup(OutputGroupInfo.TEMP_FILES)) {
             if (temp.getPath().exists()) {
-              outErr.printErrLn(
-                  "  See temp at "
-                      + OutputDirectoryLinksUtils.getPrettyPath(
-                          temp.getPath(),
-                          env.getWorkspaceName(),
-                          env.getWorkspace(),
-                          request.getBuildOptions().printWorkspaceInOutputPathsIfNeeded
-                              ? env.getWorkingDirectory()
-                              : env.getWorkspace(),
-                          request.getBuildOptions().getSymlinkPrefix(productName),
-                          productName));
+              outErr.printErrLn("  See temp at " + prettyPrinter.getPrettyPath(temp.getPath()));
             }
           }
         }
@@ -158,7 +156,7 @@
             headerFlag = false;
           }
           if (shouldPrint(importantArtifact)) {
-            outErr.printErrLn(formatArtifactForShowResults(importantArtifact, request));
+            outErr.printErrLn(formatArtifactForShowResults(prettyPrinter, importantArtifact));
           }
         }
         if (headerFlag) {
@@ -182,18 +180,8 @@
     return !artifact.isSourceArtifact() && !artifact.isMiddlemanArtifact();
   }
 
-  private String formatArtifactForShowResults(Artifact artifact, BuildRequest request) {
-    String productName = env.getRuntime().getProductName();
-    return "  "
-        + OutputDirectoryLinksUtils.getPrettyPath(
-            artifact.getPath(),
-            env.getWorkspaceName(),
-            env.getWorkspace(),
-            request.getBuildOptions().printWorkspaceInOutputPathsIfNeeded
-                ? env.getWorkingDirectory()
-                : env.getWorkspace(),
-            request.getBuildOptions().getSymlinkPrefix(productName),
-            productName);
+  private String formatArtifactForShowResults(PathPrettyPrinter prettyPrinter, Artifact artifact) {
+    return "  " + prettyPrinter.getPrettyPath(artifact.getPath());
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/OutputDirectoryLinksUtils.java b/src/main/java/com/google/devtools/build/lib/buildtool/OutputDirectoryLinksUtils.java
index 1a71624..70e7b8e 100644
--- a/src/main/java/com/google/devtools/build/lib/buildtool/OutputDirectoryLinksUtils.java
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/OutputDirectoryLinksUtils.java
@@ -39,7 +39,7 @@
  * Static utilities for managing output directory symlinks.
  */
 public class OutputDirectoryLinksUtils {
-  private static interface SymlinkDefinition {
+  static interface SymlinkDefinition {
     String getLinkName(String symlinkPrefix, String productName, String workspaceBaseName);
 
     Optional<Path> getLinkPath(
@@ -218,60 +218,10 @@
     }
   }
 
-  /**
-   * Returns a convenient path to the specified file, relativizing it and using output-dir symlinks
-   * if possible. Otherwise, return the absolute path.
-   *
-   * <p>This method must be called after the symlinks are created at the end of a build. If called
-   * before, the pretty path may be incorrect if the symlinks end up pointing somewhere new.
-   */
-  public static PathFragment getPrettyPath(
-      Path file,
-      String workspaceName,
-      Path workspaceDirectory,
-      Path workingDirectory,
-      String symlinkPrefix,
-      String productName) {
-    if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) {
-      return file.asFragment();
-    }
-
-    String workspaceBaseName = workspaceDirectory.getBaseName();
-    for (SymlinkDefinition link : LINK_DEFINITIONS) {
-      PathFragment result =
-          relativize(
-              file,
-              workspaceDirectory,
-              workingDirectory,
-              link.getLinkName(symlinkPrefix, productName, workspaceBaseName));
-      if (result != null) {
-        return result;
-      }
-    }
-
-    return file.asFragment();
-  }
-
-  // Helper to getPrettyPath.  Returns file, relativized w.r.t. the referent of
-  // "linkname", or null if it was a not a child.
-  private static PathFragment relativize(
-      Path file, Path workspaceDirectory, Path workingDirectory, String linkname) {
-    PathFragment link = PathFragment.create(linkname);
-    try {
-      Path dir = workspaceDirectory.getRelative(link);
-      PathFragment levelOneLinkTarget = dir.readSymbolicLink();
-      if (levelOneLinkTarget.isAbsolute() &&
-          file.startsWith(dir = file.getRelative(levelOneLinkTarget))) {
-        PathFragment outputLink =
-            workingDirectory.equals(workspaceDirectory)
-                ? link
-                : workspaceDirectory.getRelative(link).asFragment();
-        return outputLink.getRelative(file.relativeTo(dir));
-      }
-    } catch (IOException e) {
-      /* ignore */
-    }
-    return null;
+  public static PathPrettyPrinter getPathPrettyPrinter(
+      String symlinkPrefix, String productName, Path workspaceDirectory, Path workingDirectory) {
+    return new PathPrettyPrinter(
+        LINK_DEFINITIONS, symlinkPrefix, productName, workspaceDirectory, workingDirectory);
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/buildtool/PathPrettyPrinter.java b/src/main/java/com/google/devtools/build/lib/buildtool/PathPrettyPrinter.java
new file mode 100644
index 0000000..8d94c87
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/buildtool/PathPrettyPrinter.java
@@ -0,0 +1,99 @@
+// 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.buildtool;
+
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.buildtool.OutputDirectoryLinksUtils.SymlinkDefinition;
+import com.google.devtools.build.lib.vfs.Path;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/** Uses information about the convenience symlinks to print shorter paths for output artifacts. */
+public final class PathPrettyPrinter {
+  private static final String NO_CREATE_SYMLINKS_PREFIX = "/";
+
+  private final Map<PathFragment, Path> resolvedSymlinks;
+  private final String symlinkPrefix;
+  private final String productName;
+  private final Path workspaceDirectory;
+  private final Path workingDirectory;
+
+  /**
+   * Creates a path pretty printer, immediately resolving the symlink definitions by reading the
+   * current symlinks _from disk_.
+   */
+  PathPrettyPrinter(
+      ImmutableList<SymlinkDefinition> symlinkDefinitions,
+      String symlinkPrefix,
+      String productName,
+      Path workspaceDirectory,
+      Path workingDirectory) {
+    this.symlinkPrefix = symlinkPrefix;
+    this.productName = productName;
+    this.workspaceDirectory = workspaceDirectory;
+    this.workingDirectory = workingDirectory;
+    this.resolvedSymlinks = resolve(symlinkDefinitions);
+  }
+
+  private Map<PathFragment, Path> resolve(ImmutableList<SymlinkDefinition> symlinkDefinitions) {
+    Map<PathFragment, Path> result = new LinkedHashMap<>();
+    String workspaceBaseName = workspaceDirectory.getBaseName();
+    for (SymlinkDefinition link : symlinkDefinitions) {
+      String linkName = link.getLinkName(symlinkPrefix, productName, workspaceBaseName);
+      PathFragment linkFragment = PathFragment.create(linkName);
+      Path dir = workspaceDirectory.getRelative(linkFragment);
+      try {
+        PathFragment levelOneLinkTarget = dir.readSymbolicLink();
+        if (levelOneLinkTarget.isAbsolute()) {
+          result.put(linkFragment, dir.getRelative(levelOneLinkTarget));
+        }
+      } catch (IOException ignored) {
+        // We don't guarantee that the convenience symlinks exist - e.g., we might be running in a
+        // readonly directory. We silently fall back to printing the full path in that case. As an
+        // alternative, we could capture that information when we create the symlinks and pass that
+        // here instead of reading files back from local disk.
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Returns a convenient path to the specified file, relativizing it and using output-dir symlinks
+   * if possible. Otherwise, return the absolute path.
+   *
+   * <p>This method must be called after the symlinks are created at the end of a build. If called
+   * before, the pretty path may be incorrect if the symlinks end up pointing somewhere new.
+   */
+  public PathFragment getPrettyPath(Path file) {
+    if (NO_CREATE_SYMLINKS_PREFIX.equals(symlinkPrefix)) {
+      return file.asFragment();
+    }
+
+    for (Map.Entry<PathFragment, Path> e : resolvedSymlinks.entrySet()) {
+      PathFragment linkFragment = e.getKey();
+      Path linkTarget = e.getValue();
+      if (file.startsWith(linkTarget)) {
+        PathFragment outputLink =
+            workingDirectory.equals(workspaceDirectory)
+                ? linkFragment
+                : workspaceDirectory.getRelative(linkFragment).asFragment();
+        return outputLink.getRelative(file.relativeTo(linkTarget));
+      }
+    }
+
+    return file.asFragment();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
index 47fac97..22b38d9 100644
--- a/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
+++ b/src/main/java/com/google/devtools/build/lib/runtime/commands/RunCommand.java
@@ -39,6 +39,7 @@
 import com.google.devtools.build.lib.buildtool.BuildResult;
 import com.google.devtools.build.lib.buildtool.BuildTool;
 import com.google.devtools.build.lib.buildtool.OutputDirectoryLinksUtils;
+import com.google.devtools.build.lib.buildtool.PathPrettyPrinter;
 import com.google.devtools.build.lib.events.Event;
 import com.google.devtools.build.lib.events.Reporter;
 import com.google.devtools.build.lib.exec.ExecutionOptions;
@@ -188,16 +189,15 @@
     BuildRequestOptions requestOptions = env.getOptions().getOptions(BuildRequestOptions.class);
 
     PathFragment executablePath = executable.getPath().asFragment();
-    PathFragment prettyExecutablePath =
-        OutputDirectoryLinksUtils.getPrettyPath(
-            executable.getPath(),
-            env.getWorkspaceName(),
+    PathPrettyPrinter prettyPrinter =
+        OutputDirectoryLinksUtils.getPathPrettyPrinter(
+            requestOptions.getSymlinkPrefix(productName),
+            productName,
             env.getWorkspace(),
             requestOptions.printWorkspaceInOutputPathsIfNeeded
                 ? env.getWorkingDirectory()
-                : env.getWorkspace(),
-            requestOptions.getSymlinkPrefix(productName),
-            productName);
+                : env.getWorkspace());
+    PathFragment prettyExecutablePath = prettyPrinter.getPrettyPath(executable.getPath());
 
     RunUnder runUnder = env.getOptions().getOptions(BuildConfiguration.Options.class).runUnder;
     // Insert the command prefix specified by the "--run_under=<command-prefix>" option