Create `RegularFileStateValueWithDigest` and `RegularFileStateValueWithContentsProxy` to replace `RegularFileStateValue` to reduce memory overhead.

The `digest` and `contentsProxy` fields are mutually exclusive in `RegularFileStateValue` class so we can refactor `RegularFileStateValue` into two classes, one of which stores `digest` and the other stores `contentsProxy`.

PiperOrigin-RevId: 516938364
Change-Id: I56d473fbbe5f117c47a8953310b0edc2267571df
diff --git a/src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java b/src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java
index ca60b04..577bd24 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/FileStateValue.java
@@ -13,11 +13,12 @@
 // limitations under the License.
 package com.google.devtools.build.lib.actions;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
-import com.google.common.base.Preconditions;
 import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
 import com.google.devtools.build.lib.io.InconsistentFilesystemException;
 import com.google.devtools.build.lib.skyframe.serialization.autocodec.SerializationConstant;
@@ -101,8 +102,8 @@
         }
         return createWithStatNoFollow(
             rootedPath,
-            Preconditions.checkNotNull(FileStatusWithDigestAdapter.maybeAdapt(stat), rootedPath),
-            /*digestWillBeInjected=*/ false,
+            checkNotNull(FileStatusWithDigestAdapter.maybeAdapt(stat), rootedPath),
+            /* digestWillBeInjected= */ false,
             syscallCache,
             tsgm);
     }
@@ -120,7 +121,7 @@
     if (statNoFollow.isFile()) {
       return statNoFollow.isSpecialFile()
           ? SpecialFileStateValue.fromStat(path.asFragment(), statNoFollow, tsgm)
-          : RegularFileStateValue.fromPath(
+          : createRegularFileStateValueFromPath(
               path, statNoFollow, digestWillBeInjected, xattrProvider, tsgm);
     } else if (statNoFollow.isDirectory()) {
       return DIRECTORY_FILE_STATE_NODE;
@@ -131,6 +132,72 @@
         + "neither a file nor directory nor symlink.");
   }
 
+  /**
+   * Creates a {@link FileStateValue} instance corresponding to the given existing file.
+   *
+   * <p>We use digests only if a fast digest lookup is available from the filesystem. If not, we
+   * fall back to mtime-based digests. This avoids the case where Blaze must read all files involved
+   * in the build in order to check for modifications in the case where fast digest lookups are not
+   * available.
+   *
+   * @param stat must be of type "File". (Not a symlink).
+   */
+  private static FileStateValue createRegularFileStateValueFromPath(
+      Path path,
+      FileStatusWithDigest stat,
+      boolean digestWillBeInjected,
+      XattrProvider xattrProvider,
+      @Nullable TimestampGranularityMonitor tsgm)
+      throws InconsistentFilesystemException {
+    checkState(stat.isFile(), path);
+
+    try {
+      // If the digest will be injected, we can skip calling getFastDigest, but we need to store a
+      // contents proxy because if the digest is injected but is not available from the filesystem,
+      // we will need the proxy to determine whether the file was modified.
+      byte[] digest = digestWillBeInjected ? null : tryGetDigest(path, stat, xattrProvider);
+      if (digest == null) {
+        // Note that TimestampGranularityMonitor#notifyDependenceOnFileTime is a thread-safe method.
+        if (tsgm != null) {
+          tsgm.notifyDependenceOnFileTime(path.asFragment(), stat.getLastChangeTime());
+        }
+        return new RegularFileStateValueWithContentsProxy(
+            stat.getSize(), FileContentsProxy.create(stat));
+      } else {
+        // We are careful here to avoid putting the value ID into FileMetadata if we already have a
+        // digest. Arbitrary filesystems may do weird things with the value ID; a digest is more
+        // robust.
+        return new RegularFileStateValueWithDigest(stat.getSize(), digest);
+      }
+    } catch (IOException e) {
+      String errorMessage = e.getMessage() != null ? "error '" + e.getMessage() + "'" : "an error";
+      throw new InconsistentFilesystemException(
+          "'stat' said "
+              + path
+              + " is a file but then we "
+              + "later encountered "
+              + errorMessage
+              + " which indicates that "
+              + path
+              + " is no "
+              + "longer a file. Did you delete it during the build?");
+    }
+  }
+
+  @Nullable
+  private static byte[] tryGetDigest(
+      Path path, FileStatusWithDigest stat, XattrProvider xattrProvider) throws IOException {
+    try {
+      byte[] digest = stat.getDigest();
+      return digest != null ? digest : xattrProvider.getFastDigest(path);
+    } catch (IOException ioe) {
+      if (!path.isReadable()) {
+        return null;
+      }
+      throw ioe;
+    }
+  }
+
   @ThreadSafe
   public static RootedPath key(RootedPath rootedPath) {
     // RootedPath is already the SkyKey we want; see FileStateKey. This method and that interface
@@ -168,80 +235,89 @@
   abstract String prettyPrint();
 
   /**
-   * Implementation of {@link FileStateValue} for regular files that exist.
-   *
-   * <p>A union of (digest, mtime). We use digests only if a fast digest lookup is available from
-   * the filesystem. If not, we fall back to mtime-based digests. This avoids the case where Blaze
-   * must read all files involved in the build in order to check for modifications in the case where
-   * fast digest lookups are not available.
+   * Implementation of {@link FileStateValue} for regular files when a {@link #digest} is provided.
    */
-  @ThreadSafe
-  public static final class RegularFileStateValue extends FileStateValue {
+  public static final class RegularFileStateValueWithDigest extends FileStateValue {
     private final long size;
-    @Nullable private final byte[] digest;
-    @Nullable private final FileContentsProxy contentsProxy;
+    private final byte[] digest;
 
     @VisibleForTesting
-    public RegularFileStateValue(long size, byte[] digest, FileContentsProxy contentsProxy) {
-      Preconditions.checkState((digest == null) != (contentsProxy == null));
+    public RegularFileStateValueWithDigest(long size, byte[] digest) {
       this.size = size;
-      this.digest = digest;
-      this.contentsProxy = contentsProxy;
+      this.digest = checkNotNull(digest);
     }
 
-    /**
-     * Creates a FileFileStateValue instance corresponding to the given existing file.
-     *
-     * @param stat must be of type "File". (Not a symlink).
-     */
-    private static RegularFileStateValue fromPath(
-        Path path,
-        FileStatusWithDigest stat,
-        boolean digestWillBeInjected,
-        XattrProvider xattrProvider,
-        @Nullable TimestampGranularityMonitor tsgm)
-        throws InconsistentFilesystemException {
-      Preconditions.checkState(stat.isFile(), path);
-
-      try {
-        // If the digest will be injected, we can skip calling getFastDigest, but we need to store a
-        // contents proxy because if the digest is injected but is not available from the
-        // filesystem, we will need the proxy to determine whether the file was modified.
-        byte[] digest = digestWillBeInjected ? null : tryGetDigest(path, stat, xattrProvider);
-        if (digest == null) {
-          // Note that TimestampGranularityMonitor#notifyDependenceOnFileTime is a thread-safe
-          // method.
-          if (tsgm != null) {
-            tsgm.notifyDependenceOnFileTime(path.asFragment(), stat.getLastChangeTime());
-          }
-          return new RegularFileStateValue(stat.getSize(), null, FileContentsProxy.create(stat));
-        } else {
-          // We are careful here to avoid putting the value ID into FileMetadata if we already have
-          // a digest. Arbitrary filesystems may do weird things with the value ID; a digest is more
-          // robust.
-          return new RegularFileStateValue(stat.getSize(), digest, null);
-        }
-      } catch (IOException e) {
-        String errorMessage = e.getMessage() != null
-            ? "error '" + e.getMessage() + "'" : "an error";
-        throw new InconsistentFilesystemException("'stat' said " + path + " is a file but then we "
-            + "later encountered " + errorMessage + " which indicates that " + path + " is no "
-            + "longer a file. Did you delete it during the build?");
-      }
+    @Override
+    public FileStateType getType() {
+      return FileStateType.REGULAR_FILE;
     }
 
+    @Override
+    public long getSize() {
+      return size;
+    }
+
+    @Override
+    public byte[] getDigest() {
+      return digest;
+    }
+
+    @Override
     @Nullable
-    private static byte[] tryGetDigest(
-        Path path, FileStatusWithDigest stat, XattrProvider xattrProvider) throws IOException {
-      try {
-        byte[] digest = stat.getDigest();
-        return digest != null ? digest : xattrProvider.getFastDigest(path);
-      } catch (IOException ioe) {
-        if (!path.isReadable()) {
-          return null;
-        }
-        throw ioe;
+    public FileContentsProxy getContentsProxy() {
+      return null;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
       }
+      if (!(obj instanceof RegularFileStateValueWithDigest)) {
+        return false;
+      }
+      RegularFileStateValueWithDigest other = (RegularFileStateValueWithDigest) obj;
+      return size == other.size && Arrays.equals(digest, other.digest);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(size, Arrays.hashCode(digest));
+    }
+
+    @Override
+    public byte[] getValueFingerprint() {
+      Fingerprint fp = new Fingerprint().addLong(size);
+      fp.addBytes(digest);
+      return fp.digestAndReset();
+    }
+
+    @Override
+    public String toString() {
+      return MoreObjects.toStringHelper(this).add("digest", digest).add("size", size).toString();
+    }
+
+    @Override
+    public String prettyPrint() {
+      String contents = String.format("digest of %s", Arrays.toString(digest));
+      return String.format("regular file with size of %d and %s", size, contents);
+    }
+  }
+
+  /**
+   * Implementation of {@link FileStateValue} for regular files when {@link FileContentsProxy} is
+   * provided.
+   *
+   * <p>{@link #contentsProxy} is used to determine whether the file was modified.
+   */
+  public static final class RegularFileStateValueWithContentsProxy extends FileStateValue {
+    private final long size;
+    private final FileContentsProxy contentsProxy;
+
+    @VisibleForTesting
+    public RegularFileStateValueWithContentsProxy(long size, FileContentsProxy contentsProxy) {
+      this.size = size;
+      this.contentsProxy = checkNotNull(contentsProxy);
     }
 
     @Override
@@ -257,7 +333,7 @@
     @Override
     @Nullable
     public byte[] getDigest() {
-      return digest;
+      return null;
     }
 
     @Override
@@ -270,46 +346,37 @@
       if (obj == this) {
         return true;
       }
-      if (!(obj instanceof RegularFileStateValue)) {
+      if (!(obj instanceof RegularFileStateValueWithContentsProxy)) {
         return false;
       }
-      RegularFileStateValue other = (RegularFileStateValue) obj;
-      return size == other.size
-          && Arrays.equals(digest, other.digest)
-          && Objects.equals(contentsProxy, other.contentsProxy);
+      RegularFileStateValueWithContentsProxy other = (RegularFileStateValueWithContentsProxy) obj;
+      return size == other.size && Objects.equals(contentsProxy, other.contentsProxy);
     }
 
     @Override
     public int hashCode() {
-      return Objects.hash(size, Arrays.hashCode(digest), contentsProxy);
+      return Objects.hash(size, contentsProxy);
     }
 
     @Override
     public byte[] getValueFingerprint() {
       Fingerprint fp = new Fingerprint().addLong(size);
-      if (digest != null) {
-        fp.addBytes(digest);
-      }
-      if (contentsProxy != null) {
-        contentsProxy.addToFingerprint(fp);
-      }
+      contentsProxy.addToFingerprint(fp);
       return fp.digestAndReset();
     }
 
     @Override
     public String toString() {
       return MoreObjects.toStringHelper(this)
-          .add("digest", digest)
           .add("size", size)
-          .add("contentsProxy", contentsProxy).toString();
+          .add("contentsProxy", contentsProxy)
+          .toString();
     }
 
     @Override
     public String prettyPrint() {
-      String contents = digest != null
-          ? String.format("digest of %s", Arrays.toString(digest))
-          : contentsProxy.prettyPrint();
-      return String.format("regular file with size of %d and %s", size, contents);
+      return String.format(
+          "regular file with size of %d and %s", size, contentsProxy.prettyPrint());
     }
   }
 
@@ -320,7 +387,7 @@
 
     @VisibleForTesting
     public SpecialFileStateValue(FileContentsProxy contentsProxy) {
-      this.contentsProxy = Preconditions.checkNotNull(contentsProxy);
+      this.contentsProxy = checkNotNull(contentsProxy);
     }
 
     private static SpecialFileStateValue fromStat(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
index ad69283..3dc808c 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/RecursiveFilesystemTraversalFunction.java
@@ -13,10 +13,12 @@
 // limitations under the License.
 package com.google.devtools.build.lib.skyframe;
 
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
 import com.google.common.base.Verify;
 import com.google.common.collect.ImmutableList;
 import com.google.devtools.build.lib.actions.Artifact;
@@ -24,7 +26,8 @@
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.FileStateType;
 import com.google.devtools.build.lib.actions.FileStateValue;
-import com.google.devtools.build.lib.actions.FileStateValue.RegularFileStateValue;
+import com.google.devtools.build.lib.actions.FileStateValue.RegularFileStateValueWithContentsProxy;
+import com.google.devtools.build.lib.actions.FileStateValue.RegularFileStateValueWithDigest;
 import com.google.devtools.build.lib.actions.FileValue;
 import com.google.devtools.build.lib.actions.HasDigest;
 import com.google.devtools.build.lib.collect.nestedset.NestedSet;
@@ -105,14 +108,15 @@
       SYMLINK_CYCLE_OR_INFINITE_EXPANSION,
     }
 
-    private final Type type;
+    private final RecursiveFilesystemTraversalException.Type type;
 
-    RecursiveFilesystemTraversalException(String message, Type type) {
+    RecursiveFilesystemTraversalException(
+        String message, RecursiveFilesystemTraversalException.Type type) {
       super(message);
       this.type = type;
     }
 
-    public Type getType() {
+    public RecursiveFilesystemTraversalException.Type getType() {
       return type;
     }
   }
@@ -129,9 +133,9 @@
       super(
           String.format(
               "Found dangling symlink: %s, unresolved path: \"%s\"", path, unresolvedLink),
-          Type.DANGLING_SYMLINK);
-      Preconditions.checkArgument(path != null && !path.isEmpty());
-      Preconditions.checkArgument(unresolvedLink != null && !unresolvedLink.isEmpty());
+          RecursiveFilesystemTraversalException.Type.DANGLING_SYMLINK);
+      checkArgument(path != null && !path.isEmpty());
+      checkArgument(unresolvedLink != null && !unresolvedLink.isEmpty());
     }
   }
 
@@ -288,8 +292,8 @@
         HasDigest metadata,
         @Nullable RootedPath realPath,
         @Nullable PathFragment unresolvedSymlinkTarget) {
-      Preconditions.checkNotNull(metadata.getDigest(), metadata);
-      this.type = Preconditions.checkNotNull(type);
+      checkNotNull(metadata.getDigest(), metadata);
+      this.type = checkNotNull(type);
       this.metadata = metadata;
       this.realPath = realPath;
       this.unresolvedSymlinkTarget = unresolvedSymlinkTarget;
@@ -330,7 +334,7 @@
       }
       RootedPath realPath = traversal.root().asRootedPath();
       if (traversal.strictOutputFiles()) {
-        Preconditions.checkNotNull(fsVal, "Strict Fileset output tree has null FileArtifactValue");
+        checkNotNull(fsVal, "Strict Fileset output tree has null FileArtifactValue");
         return new FileInfo(
             fsVal instanceof TreeArtifactValue ? FileType.DIRECTORY : FileType.FILE,
             fsVal,
@@ -451,15 +455,14 @@
       throws IOException {
     if (fsVal instanceof FileStateValue) {
       FileStateValue fsv = (FileStateValue) fsVal;
-      if (fsv instanceof RegularFileStateValue) {
-        RegularFileStateValue rfsv = (RegularFileStateValue) fsv;
-        return rfsv.getDigest() != null
-            // If we have the digest, then simply convert it with the digest value.
-            ? FileArtifactValue.createForVirtualActionInput(rfsv.getDigest(), rfsv.getSize())
-            // Otherwise, create a file FileArtifactValue (RegularFileArtifactValue) based on the
-            // path and size.
-            : FileArtifactValue.createForNormalFileUsingPath(path, rfsv.getSize(), syscallCache);
+      if (fsv instanceof RegularFileStateValueWithDigest) {
+        RegularFileStateValueWithDigest rfsv = (RegularFileStateValueWithDigest) fsv;
+        return FileArtifactValue.createForVirtualActionInput(rfsv.getDigest(), rfsv.getSize());
+      } else if (fsv instanceof RegularFileStateValueWithContentsProxy) {
+        RegularFileStateValueWithContentsProxy rfsv = (RegularFileStateValueWithContentsProxy) fsv;
+        return FileArtifactValue.createForNormalFileUsingPath(path, rfsv.getSize(), syscallCache);
       }
+
       return new HasDigest.ByteStringDigest(fsv.getValueFingerprint());
     } else if (fsVal instanceof FileArtifactValue) {
       FileArtifactValue fav = ((FileArtifactValue) fsVal);
@@ -481,37 +484,38 @@
       CONFLICT, DIRECTORY, PKG
     }
 
-    private final Type type;
+    private final PkgLookupResult.Type type;
     final TraversalRequest traversal;
     final FileInfo rootInfo;
 
     /** Result for a generated directory that conflicts with a source package. */
     static PkgLookupResult conflict(TraversalRequest traversal, FileInfo rootInfo) {
-      return new PkgLookupResult(Type.CONFLICT, traversal, rootInfo);
+      return new PkgLookupResult(PkgLookupResult.Type.CONFLICT, traversal, rootInfo);
     }
 
     /** Result for a source or generated directory (not a package). */
     static PkgLookupResult directory(TraversalRequest traversal, FileInfo rootInfo) {
-      return new PkgLookupResult(Type.DIRECTORY, traversal, rootInfo);
+      return new PkgLookupResult(PkgLookupResult.Type.DIRECTORY, traversal, rootInfo);
     }
 
     /** Result for a package, i.e. a directory  with a BUILD file. */
     static PkgLookupResult pkg(TraversalRequest traversal, FileInfo rootInfo) {
-      return new PkgLookupResult(Type.PKG, traversal, rootInfo);
+      return new PkgLookupResult(PkgLookupResult.Type.PKG, traversal, rootInfo);
     }
 
-    private PkgLookupResult(Type type, TraversalRequest traversal, FileInfo rootInfo) {
-      this.type = Preconditions.checkNotNull(type);
-      this.traversal = Preconditions.checkNotNull(traversal);
-      this.rootInfo = Preconditions.checkNotNull(rootInfo);
+    private PkgLookupResult(
+        PkgLookupResult.Type type, TraversalRequest traversal, FileInfo rootInfo) {
+      this.type = checkNotNull(type);
+      this.traversal = checkNotNull(traversal);
+      this.rootInfo = checkNotNull(rootInfo);
     }
 
     boolean isPackage() {
-      return type == Type.PKG;
+      return type == PkgLookupResult.Type.PKG;
     }
 
     boolean isConflicting() {
-      return type == Type.CONFLICT;
+      return type == PkgLookupResult.Type.CONFLICT;
     }
 
     @Override
@@ -531,8 +535,8 @@
   private static PkgLookupResult checkIfPackage(
       Environment env, TraversalRequest traversal, FileInfo rootInfo, SyscallCache syscallCache)
       throws IOException, InterruptedException, BuildFileNotFoundException {
-    Preconditions.checkArgument(rootInfo.type.exists() && !rootInfo.type.isFile(),
-        "{%s} {%s}", traversal, rootInfo);
+    checkArgument(
+        rootInfo.type.exists() && !rootInfo.type.isFile(), "{%s} {%s}", traversal, rootInfo);
     // PackageLookupFunction/dependencies can only throw IOException, BuildFileNotFoundException,
     // and RepositoryFetchException, and RepositoryFetchException is not in play here. Note that
     // run-of-the-mill circular symlinks will *not* throw here, and will trigger later errors during
@@ -579,8 +583,7 @@
    */
   private static RecursiveFilesystemTraversalValue resultForDanglingSymlink(
       RootedPath linkName, FileInfo info) {
-    Preconditions.checkState(info.type.isSymlink() && !info.type.exists(), "{%s} {%s}", linkName,
-        info.type);
+    checkState(info.type.isSymlink() && !info.type.exists(), "{%s} {%s}", linkName, info.type);
     return RecursiveFilesystemTraversalValue.of(
         ResolvedFileFactory.danglingSymlink(linkName, info.unresolvedSymlinkTarget, info.metadata));
   }
@@ -593,8 +596,7 @@
    */
   private static RecursiveFilesystemTraversalValue resultForFileRoot(
       RootedPath path, FileInfo info) {
-    Preconditions.checkState(
-        info.type.isFile() && info.type.exists(), "{%s} {%s}", path, info.type);
+    checkState(info.type.isFile() && info.type.exists(), "{%s} {%s}", path, info.type);
     if (info.type.isSymlink()) {
       return RecursiveFilesystemTraversalValue.of(
           ResolvedFileFactory.symlinkToFile(