Add an interner for non source artifacts that are deserialized.

PiperOrigin-RevId: 202311773
diff --git a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
index a974646..a27756e 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/Artifact.java
@@ -22,6 +22,8 @@
 import com.google.common.base.Joiner;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Predicate;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Streams;
@@ -53,6 +55,7 @@
 import java.util.Comparator;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.ExecutionException;
 import javax.annotation.Nullable;
 
 /**
@@ -152,6 +155,9 @@
   /** A Predicate that evaluates to true if the Artifact is not a middleman artifact. */
   public static final Predicate<Artifact> MIDDLEMAN_FILTER = input -> !input.isMiddlemanArtifact();
 
+  private static final Cache<InternedArtifact, Artifact> ARTIFACT_INTERNER =
+      CacheBuilder.newBuilder().weakValues().build();
+
   private final int hashCode;
   private final ArtifactRoot root;
   private final PathFragment execPath;
@@ -177,11 +183,24 @@
               + ")");
     }
     PathFragment rootExecPath = root.getExecPath();
-    return new Artifact(
+    Artifact artifact = new Artifact(
         root,
         rootExecPath.isEmpty() ? rootRelativePath : rootExecPath.getRelative(rootRelativePath),
         rootRelativePath,
         owner);
+    if (artifact.isSourceArtifact()) {
+      return artifact;
+    } else {
+      return intern(artifact);
+    }
+  }
+
+  private static Artifact intern(Artifact artifact) {
+    try {
+      return ARTIFACT_INTERNER.get(new InternedArtifact(artifact), () -> artifact);
+    } catch (ExecutionException e) {
+      throw new IllegalStateException(e);
+    }
   }
 
   /**
@@ -583,6 +602,34 @@
   }
 
   /**
+   * Wraps an artifact for interning because we need to check the artifact owner when doing equals.
+   */
+  private static final class InternedArtifact {
+    private final Artifact wrappedArtifact;
+
+    InternedArtifact(Artifact artifact) {
+      this.wrappedArtifact = artifact;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof InternedArtifact)) {
+        return false;
+      }
+      if (!getClass().equals(other.getClass())) {
+        return false;
+      }
+      InternedArtifact that = (InternedArtifact) other;
+      return Artifact.equalWithOwner(wrappedArtifact, that.wrappedArtifact);
+    }
+
+    @Override
+    public final int hashCode() {
+      return wrappedArtifact.hashCode();
+    }
+  }
+
+  /**
    * Returns the relative path to this artifact relative to its root.  (Useful
    * when deriving output filenames from input files, etc.)
    */