When trimming BuildOptions, return the same instance if there are no changes.

On a typical build, thousands of value-equal BuildOptions instances are being created. In addition to consuming memory, this is causing BuildOptions to go through its (relatively expensive) hashCode/fingerprint initialization many times.

An added bonus comes from not calling toString on the FragmentOptions instance, which requires reflectively iterating over all of its options fields.

PiperOrigin-RevId: 244853750
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
index 1838331..a8ac70f 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/config/BuildOptions.java
@@ -23,6 +23,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.common.collect.MapDifference;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
@@ -91,19 +92,29 @@
   }
 
   /**
-   * Returns an equivalent instance to this one with only options from the given {@link
-   * FragmentOptions} classes.
+   * Returns {@code BuildOptions} that are otherwise identical to this one, but contain only options
+   * from the given {@link FragmentOptions} classes (plus build configuration options).
+   *
+   * <p>If nothing needs to be trimmed, this instance is returned.
    */
   public BuildOptions trim(Set<Class<? extends FragmentOptions>> optionsClasses) {
-    Builder builder = builder();
+    List<FragmentOptions> retainedOptions =
+        Lists.newArrayListWithExpectedSize(optionsClasses.size() + 1);
     for (FragmentOptions options : fragmentOptionsMap.values()) {
       if (optionsClasses.contains(options.getClass())
           // TODO(bazel-team): make this non-hacky while not requiring BuildConfiguration access
           // to BuildOptions.
-          || options.toString().contains("BuildConfiguration$Options")) {
-        builder.addFragmentOptions(options);
+          || options.getClass().getName().endsWith("BuildConfiguration$Options")) {
+        retainedOptions.add(options);
       }
     }
+    if (retainedOptions.size() == fragmentOptionsMap.size()) {
+      return this; // Nothing to trim.
+    }
+    Builder builder = builder();
+    for (FragmentOptions options : retainedOptions) {
+      builder.addFragmentOptions(options);
+    }
     return builder.addStarlarkOptions(skylarkOptionsMap).build();
   }
 
diff --git a/src/test/java/com/google/devtools/build/lib/analysis/config/BuildOptionsTest.java b/src/test/java/com/google/devtools/build/lib/analysis/config/BuildOptionsTest.java
index db47f96..326b2eb 100644
--- a/src/test/java/com/google/devtools/build/lib/analysis/config/BuildOptionsTest.java
+++ b/src/test/java/com/google/devtools/build/lib/analysis/config/BuildOptionsTest.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Maps;
 import com.google.devtools.build.lib.analysis.config.BuildConfiguration.Options;
 import com.google.devtools.build.lib.analysis.config.BuildOptions.OptionsDiff;
@@ -507,4 +508,28 @@
 
     assertThat(original.matches(parser)).isFalse();
   }
+
+  @Test
+  public void trim() throws Exception {
+    BuildOptions original = BuildOptions.of(ImmutableList.of(CppOptions.class, JavaOptions.class));
+    BuildOptions trimmed = original.trim(ImmutableSet.of(CppOptions.class));
+    assertThat(trimmed.getFragmentClasses()).containsExactly(CppOptions.class);
+  }
+
+  @Test
+  public void trim_retainsBuildConfigurationOptions() throws Exception {
+    BuildOptions original =
+        BuildOptions.of(
+            ImmutableList.of(
+                CppOptions.class, JavaOptions.class, BuildConfiguration.Options.class));
+    BuildOptions trimmed = original.trim(ImmutableSet.of());
+    assertThat(trimmed.getFragmentClasses()).containsExactly(BuildConfiguration.Options.class);
+  }
+
+  @Test
+  public void trim_nothingTrimmed_returnsSameInstance() throws Exception {
+    BuildOptions original = BuildOptions.of(ImmutableList.of(CppOptions.class, JavaOptions.class));
+    BuildOptions trimmed = original.trim(ImmutableSet.of(CppOptions.class, JavaOptions.class));
+    assertThat(trimmed).isSameAs(original);
+  }
 }