Improve performance of OptionsBase#asMap.

Much of the improvement comes from avoiding calls to OptionDefinition#extractOptionDefinition which is expensive due to its reflective annotation access. The previous methodology for obtaining an option's value was going from OptionsBase -> OptionDefinition (cached) -> Field -> OptionDefinition (uncached) -> Field -> Object. Now it simply goes OptionsBase -> OptionDefinition (cached) -> Field -> Object.

PiperOrigin-RevId: 243304970
diff --git a/src/main/java/com/google/devtools/common/options/OptionsBase.java b/src/main/java/com/google/devtools/common/options/OptionsBase.java
index ce66e66..fd684c6 100644
--- a/src/main/java/com/google/devtools/common/options/OptionsBase.java
+++ b/src/main/java/com/google/devtools/common/options/OptionsBase.java
@@ -14,10 +14,9 @@
 
 package com.google.devtools.common.options;
 
+import com.google.common.collect.Maps;
 import com.google.common.escape.CharEscaperBuilder;
 import com.google.common.escape.Escaper;
-import java.lang.reflect.Field;
-import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
@@ -63,23 +62,23 @@
    * inherited ones. The mapping is a copy, so subsequent mutations to it or to this object are
    * independent. Entries are sorted alphabetically.
    */
-  public final <O extends OptionsBase> Map<String, Object> asMap() {
-    // Generic O is needed to tell the type system that the toMap() call is safe.
-    // The casts are safe because "this" is an instance of "getClass()"
-    // which subclasses OptionsBase.
-    @SuppressWarnings("unchecked")
-    O castThis = (O) this;
-    @SuppressWarnings("unchecked")
-    Class<O> castClass = (Class<O>) getClass();
-
-    Map<String, Object> map = new LinkedHashMap<>();
-    for (Map.Entry<Field, Object> entry : OptionsParser.toMap(castClass, castThis).entrySet()) {
-      OptionDefinition optionDefinition = OptionDefinition.extractOptionDefinition(entry.getKey());
-      map.put(optionDefinition.getOptionName(), entry.getValue());
+  public final Map<String, Object> asMap() {
+    List<OptionDefinition> definitions = OptionsData.getAllOptionDefinitionsForClass(getClass());
+    Map<String, Object> map = Maps.newLinkedHashMapWithExpectedSize(definitions.size());
+    for (OptionDefinition definition : definitions) {
+      map.put(definition.getOptionName(), getValueFromDefinition(definition));
     }
     return map;
   }
 
+  private Object getValueFromDefinition(OptionDefinition definition) {
+    try {
+      return definition.getField().get(this);
+    } catch (IllegalAccessException e) {
+      throw new IllegalStateException("All options fields of options classes should be public", e);
+    }
+  }
+
   @Override
   public final String toString() {
     return getClass().getName() + asMap();
diff --git a/src/main/java/com/google/devtools/common/options/OptionsParser.java b/src/main/java/com/google/devtools/common/options/OptionsParser.java
index 78f37e0..4f26562 100644
--- a/src/main/java/com/google/devtools/common/options/OptionsParser.java
+++ b/src/main/java/com/google/devtools/common/options/OptionsParser.java
@@ -23,19 +23,14 @@
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.MoreCollectors;
 import com.google.common.escape.Escaper;
-import com.google.devtools.common.options.OptionDefinition.NotAnOptionException;
 import com.google.devtools.common.options.OptionsParserImpl.ResidueAndPriority;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -796,132 +791,5 @@
     OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
     return data.getUsesOnlyCoreTypes(optionsClass);
   }
-
-  /**
-   * Returns a mapping from each option {@link Field} in {@code optionsClass} (including inherited
-   * ones) to its value in {@code options}.
-   *
-   * <p>To save space, the map directly stores {@code Fields} instead of the {@code
-   * OptionDefinitions}.
-   *
-   * <p>The map is a mutable copy; changing the map won't affect {@code options} and vice versa. The
-   * map entries appear sorted alphabetically by option name.
-   *
-   * <p>If {@code options} is an instance of a subclass of {@link OptionsBase}, any options defined
-   * by the subclass are not included in the map, only the options declared in the provided class
-   * are included.
-   *
-   * @throws IllegalArgumentException if {@code options} is not an instance of {@link OptionsBase}
-   */
-  public static <O extends OptionsBase> Map<Field, Object> toMap(Class<O> optionsClass, O options) {
-    // Alphabetized due to getAllOptionDefinitionsForClass()'s order.
-    Map<Field, Object> map = new LinkedHashMap<>();
-    for (OptionDefinition optionDefinition :
-        OptionsData.getAllOptionDefinitionsForClass(optionsClass)) {
-      try {
-        // Get the object value of the optionDefinition and place in map.
-        map.put(optionDefinition.getField(), optionDefinition.getField().get(options));
-      } catch (IllegalAccessException e) {
-        // All options fields of options classes should be public.
-        throw new IllegalStateException(e);
-      } catch (IllegalArgumentException e) {
-        // This would indicate an inconsistency in the cached OptionsData.
-        throw new IllegalStateException(e);
-      }
-    }
-    return map;
-  }
-
-  /**
-   * Given a mapping as returned by {@link #toMap}, and the options class it that its entries
-   * correspond to, this constructs the corresponding instance of the options class.
-   *
-   * @param map Field to Object, expecting an entry for each field in the optionsClass. This
-   *     directly refers to the Field, without wrapping it in an OptionDefinition, see {@link
-   *     #toMap}.
-   * @throws IllegalArgumentException if {@code map} does not contain exactly the fields of {@code
-   *     optionsClass}, with values of the appropriate type
-   */
-  public static <O extends OptionsBase> O fromMap(Class<O> optionsClass, Map<Field, Object> map) {
-    // Instantiate the options class.
-    OptionsData data = getOptionsDataInternal(optionsClass);
-    O optionsInstance;
-    try {
-      Constructor<O> constructor = data.getConstructor(optionsClass);
-      Preconditions.checkNotNull(constructor, "No options class constructor available");
-      optionsInstance = constructor.newInstance();
-    } catch (ReflectiveOperationException e) {
-      throw new IllegalStateException("Error while instantiating options class", e);
-    }
-
-    List<OptionDefinition> optionDefinitions =
-        OptionsData.getAllOptionDefinitionsForClass(optionsClass);
-    // Ensure all fields are covered, no extraneous fields.
-    validateFieldsSets(optionsClass, new LinkedHashSet<Field>(map.keySet()));
-    // Populate the instance.
-    for (OptionDefinition optionDefinition : optionDefinitions) {
-      // Non-null as per above check.
-      Object value = map.get(optionDefinition.getField());
-      try {
-        optionDefinition.getField().set(optionsInstance, value);
-      } catch (IllegalAccessException e) {
-        throw new IllegalStateException(e);
-      }
-      // May also throw IllegalArgumentException if map value is ill typed.
-    }
-    return optionsInstance;
-  }
-
-  /**
-   * Raises a pretty {@link IllegalArgumentException} if the provided set of fields is a complete
-   * set for the optionsClass.
-   *
-   * <p>The entries in {@code fieldsFromMap} may be ill formed by being null or lacking an {@link
-   * Option} annotation.
-   */
-  private static void validateFieldsSets(
-      Class<? extends OptionsBase> optionsClass, LinkedHashSet<Field> fieldsFromMap) {
-    ImmutableList<OptionDefinition> optionDefsFromClasses =
-        OptionsData.getAllOptionDefinitionsForClass(optionsClass);
-    Set<Field> fieldsFromClass =
-        optionDefsFromClasses.stream().map(OptionDefinition::getField).collect(Collectors.toSet());
-
-    if (fieldsFromClass.equals(fieldsFromMap)) {
-      // They are already equal, avoid additional checks.
-      return;
-    }
-
-    List<String> extraNamesFromClass = new ArrayList<>();
-    List<String> extraNamesFromMap = new ArrayList<>();
-    for (OptionDefinition optionDefinition : optionDefsFromClasses) {
-      if (!fieldsFromMap.contains(optionDefinition.getField())) {
-        extraNamesFromClass.add("'" + optionDefinition.getOptionName() + "'");
-      }
-    }
-    for (Field field : fieldsFromMap) {
-      // Extra validation on the map keys since they don't come from OptionsData.
-      if (!fieldsFromClass.contains(field)) {
-        if (field == null) {
-          extraNamesFromMap.add("<null field>");
-        } else {
-          OptionDefinition optionDefinition = null;
-          try {
-            // TODO(ccalvarin) This shouldn't be necessary, no option definitions should be found in
-            // this optionsClass that weren't in the cache.
-            optionDefinition = OptionDefinition.extractOptionDefinition(field);
-            extraNamesFromMap.add("'" + optionDefinition.getOptionName() + "'");
-          } catch (NotAnOptionException e) {
-            extraNamesFromMap.add("<non-Option field>");
-          }
-        }
-      }
-    }
-    throw new IllegalArgumentException(
-        "Map keys do not match fields of options class; extra map keys: {"
-            + Joiner.on(", ").join(extraNamesFromMap)
-            + "}; extra options class options: {"
-            + Joiner.on(", ").join(extraNamesFromClass)
-            + "}");
-  }
 }
 
diff --git a/src/test/java/com/google/devtools/common/options/OptionsMapConversionTest.java b/src/test/java/com/google/devtools/common/options/OptionsMapConversionTest.java
index b96e57a..9065912 100644
--- a/src/test/java/com/google/devtools/common/options/OptionsMapConversionTest.java
+++ b/src/test/java/com/google/devtools/common/options/OptionsMapConversionTest.java
@@ -15,12 +15,7 @@
 package com.google.devtools.common.options;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.fail;
 
-import com.google.common.collect.ImmutableMap;
-import java.lang.reflect.Field;
-import java.util.LinkedHashMap;
-import java.util.Map;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -29,90 +24,44 @@
 @RunWith(JUnit4.class)
 public class OptionsMapConversionTest {
 
-  private static Map<String, Object> keysToStrings(Map<Field, Object> map) {
-    Map<String, Object> result = new LinkedHashMap<>();
-    for (Map.Entry<Field, Object> entry : map.entrySet()) {
-      OptionDefinition optionDefinition = OptionDefinition.extractOptionDefinition(entry.getKey());
-      result.put(optionDefinition.getOptionName(), entry.getValue());
-    }
-    return result;
-  }
-
-  private static Map<Field, Object> keysToFields(
-      Class<? extends OptionsBase> optionsClass, Map<String, Object> map) {
-    OptionsData data = OptionsParser.getOptionsDataInternal(optionsClass);
-    Map<Field, Object> result = new LinkedHashMap<>();
-    for (Map.Entry<String, Object> entry : map.entrySet()) {
-      OptionDefinition optionDefinition = data.getOptionDefinitionFromName(entry.getKey());
-      result.put(optionDefinition.getField(), entry.getValue());
-    }
-    return result;
-  }
-
   /** Dummy options base class. */
   public static class FooOptions extends OptionsBase {
     @Option(
-      name = "foo",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "false"
-    )
+        name = "foo",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "false")
     public boolean foo;
   }
 
   /** Dummy options derived class. */
   public static class BazOptions extends FooOptions {
     @Option(
-      name = "bar",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "true"
-    )
+        name = "bar",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "true")
     public boolean bar;
 
     @Option(
-      name = "baz",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "5"
-    )
+        name = "baz",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "5")
     public int baz;
   }
 
   @Test
-  public void toMap_Basic() {
-    FooOptions foo = Options.getDefaults(FooOptions.class);
-    assertThat(keysToStrings(OptionsParser.toMap(FooOptions.class, foo)))
-        .containsExactly("foo", false);
-  }
-
-  @Test
   public void asMap_Basic() {
     FooOptions foo = Options.getDefaults(FooOptions.class);
-    assertThat(foo.asMap())
-        .containsExactly("foo", false);
-  }
-
-  @Test
-  public void toMap_Inheritance() {
-    BazOptions baz = Options.getDefaults(BazOptions.class);
-    assertThat(keysToStrings(OptionsParser.toMap(BazOptions.class, baz)))
-        .containsExactly("foo", false, "bar", true, "baz", 5);
+    assertThat(foo.asMap()).containsExactly("foo", false);
   }
 
   @Test
   public void asMap_Inheritance() {
     // Static type is base class, dynamic type is derived. We still get the derived fields.
     FooOptions foo = Options.getDefaults(BazOptions.class);
-    assertThat(foo.asMap())
-        .containsExactly("foo", false, "bar", true, "baz", 5);
-  }
-
-  @Test
-  public void toMap_InheritanceBaseFieldsOnly() {
-    BazOptions baz = Options.getDefaults(BazOptions.class);
-    assertThat(keysToStrings(OptionsParser.toMap(FooOptions.class, baz)))
-        .containsExactly("foo", false);
+    assertThat(foo.asMap()).containsExactly("foo", false, "bar", true, "baz", 5);
   }
 
   /**
@@ -123,135 +72,44 @@
   public static class AlphaOptions extends OptionsBase {
 
     @Option(
-      name = "c",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "0"
-    )
+        name = "c",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "0")
     public int v;
 
     @Option(
-      name = "d",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "0"
-    )
+        name = "d",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "0")
     public int w;
 
     @Option(
-      name = "a",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "0"
-    )
+        name = "a",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "0")
     public int x;
 
     @Option(
-      name = "e",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "0"
-    )
+        name = "e",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "0")
     public int y;
 
     @Option(
-      name = "b",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "0"
-    )
+        name = "b",
+        documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+        effectTags = {OptionEffectTag.NO_OP},
+        defaultValue = "0")
     public int z;
   }
 
   @Test
-  public void toMap_AlphabeticalOrder() {
-    AlphaOptions alpha = Options.getDefaults(AlphaOptions.class);
-    assertThat(keysToStrings(OptionsParser.toMap(AlphaOptions.class, alpha)))
-        .containsExactly("a", 0, "b", 0, "c", 0, "d", 0, "e", 0).inOrder();
-  }
-
-  @Test
   public void asMap_AlphabeticalOrder() {
     AlphaOptions alpha = Options.getDefaults(AlphaOptions.class);
-    assertThat(alpha.asMap())
-        .containsExactly("a", 0, "b", 0, "c", 0, "d", 0, "e", 0).inOrder();
-  }
-
-  @Test
-  public void fromMap_Basic() {
-    Map<String, Object> map = ImmutableMap.<String, Object>of("foo", true);
-    Map<Field, Object> fieldMap = keysToFields(FooOptions.class, map);
-    FooOptions foo = OptionsParser.fromMap(FooOptions.class, fieldMap);
-    assertThat(foo.foo).isTrue();
-  }
-
-  /** Dummy subclass of foo. */
-  public static class SubFooAOptions extends FooOptions {
-    @Option(
-      name = "a",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "false"
-    )
-    public boolean a;
-  }
-
-  /** Dummy subclass of foo. */
-  public static class SubFooBOptions extends FooOptions {
-    @Option(
-      name = "b1",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "false"
-    )
-    public boolean b1;
-
-    @Option(
-      name = "b2",
-      documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
-      effectTags = {OptionEffectTag.NO_OP},
-      defaultValue = "false"
-    )
-    public boolean b2;
-  }
-
-  @Test
-  public void fromMap_FailsOnWrongKeys() {
-    Map<String, Object> map = ImmutableMap.<String, Object>of("foo", true, "a", false);
-    Map<Field, Object> fieldMap = keysToFields(SubFooAOptions.class, map);
-    try {
-      OptionsParser.fromMap(SubFooBOptions.class, fieldMap);
-      fail("Should have failed due to the given map's fields not matching the ones on the class");
-    } catch (IllegalArgumentException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .contains(
-              "Map keys do not match fields of options class; extra map keys: {'a'}; "
-                  + "extra options class options: {'b1', 'b2'}");
-    }
-  }
-
-  @Test
-  public void fromMap_FailsOnWrongTypes() {
-    Map<String, Object> map = ImmutableMap.<String, Object>of("foo", 5);
-    Map<Field, Object> fieldMap = keysToFields(SubFooAOptions.class, map);
-    try {
-      OptionsParser.fromMap(FooOptions.class, fieldMap);
-      fail("Should have failed due to trying to assign a field value with the wrong type");
-    } catch (IllegalArgumentException e) {
-      assertThat(e)
-          .hasMessageThat()
-          .matches("Can not set boolean field .*\\.foo to java\\.lang\\.Integer");
-    }
-  }
-
-  @Test
-  public void fromMap_Inheritance() {
-    Map<String, Object> map = ImmutableMap.<String, Object>of("foo", true, "bar", true, "baz", 3);
-    Map<Field, Object> fieldMap = keysToFields(BazOptions.class, map);
-    BazOptions baz = OptionsParser.fromMap(BazOptions.class, fieldMap);
-    assertThat(baz.foo).isTrue();
-    assertThat(baz.bar).isTrue();
-    assertThat(baz.baz).isEqualTo(3);
+    assertThat(alpha.asMap()).containsExactly("a", 0, "b", 0, "c", 0, "d", 0, "e", 0).inOrder();
   }
 }