[ApiModel] Add new database design and lookup

This will improve the speed of initial loading considerably since we only read a continuous byte array and do not build up any maps. In a follow up we may be able to do direct indexing into the R8 jar for "pay-as-you-go" cost for looking up api levels.

Bug: b/213552119
Bug: b/207452750
Change-Id: Icd9479805df5952124c88e534fb51069179188d8
diff --git a/.gitignore b/.gitignore
index ceddaef..b19c671 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,8 @@
 tests/2016-12-19/art.tar.gz
 tests/2017-10-04/art
 tests/2017-10-04/art.tar.gz
+third_party/api_database/api_database
+third_party/api_database/api_database.tar.gz
 third_party/api-outlining/simple-app-dump
 third_party/api-outlining/simple-app-dump.tar.gz
 third_party/android_cts_baseline
diff --git a/build.gradle b/build.gradle
index cbe0747..d157084 100644
--- a/build.gradle
+++ b/build.gradle
@@ -62,12 +62,20 @@
 
 // Custom source set for example tests and generated tests.
 sourceSets {
+    main {
+        java {
+            srcDirs = ['src/main/java']
+        }
+        resources {
+            srcDirs "third_party/api_database/api_database"
+        }
+    }
     main11 {
         java {
             srcDirs = ['src/main/java']
         }
         resources {
-            srcDirs = ['src/main/resources']
+            srcDirs "third_party/api_database/api_database"
         }
     }
     test {
@@ -311,6 +319,7 @@
                 "android_jar/lib-v31",
                 "android_jar/lib-v32",
                 "android_jar/lib-v33",
+                "api_database/api_database",
                 "api-outlining/simple-app-dump",
                 "binary_compatibility_tests/compiler_api_tests",
                 "core-lambda-stubs",
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiDataAccess.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiDataAccess.java
new file mode 100644
index 0000000..74b78c4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiDataAccess.java
@@ -0,0 +1,316 @@
+// Copyright (c) 2022, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.androidapi;
+
+import static com.android.tools.r8.lightir.ByteUtils.unsetBitAtIndex;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
+import com.google.common.io.ByteStreams;
+import com.google.common.primitives.Ints;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.function.BiPredicate;
+
+/**
+ * Implements low-level access methods for seeking on top of the database file defined by {@code
+ * AndroidApiLevelHashingDatabaseImpl} where a description of the format can also be found.
+ */
+public abstract class AndroidApiDataAccess {
+
+  private static final String RESOURCE_NAME = "resources/new_api_database.ser";
+  private static final int ENTRY_SIZE_IN_BITS_FOR_CONSTANT_POOL_MAP = 17;
+  private static final int ENTRY_SIZE_IN_BITS_FOR_API_MAP = 18;
+  // The payload offset is an offset into the payload defined by an integer and a length defined by
+  // a short.
+  private static final int PAYLOAD_OFFSET_WITH_LENGTH = 4 + 2;
+  private static final byte ZERO_BYTE = (byte) 0;
+
+  private static class PositionAndLength {
+
+    private static PositionAndLength EMPTY = new PositionAndLength(0, 0);
+
+    private final int position;
+    private final int length;
+
+    private PositionAndLength(int position, int length) {
+      this.position = position;
+      this.length = length;
+    }
+
+    public static PositionAndLength create(int position, int length) {
+      if (position == 0 && length == 0) {
+        return EMPTY;
+      }
+      if ((position > 0 && length > 0) || (position < 0 && length == 0)) {
+        assert false : "Unexpected position and length";
+        return EMPTY;
+      }
+      return new PositionAndLength(position, length);
+    }
+
+    public static PositionAndLength create(byte[] data, int offset) {
+      return create(readIntFromOffset(data, offset), readShortFromOffset(data, offset + 4));
+    }
+
+    public int getPosition() {
+      return position;
+    }
+
+    public int getLength() {
+      return length;
+    }
+
+    public boolean isEmpty() {
+      return this == EMPTY;
+    }
+  }
+
+  public static int entrySizeInBitsForConstantPoolMap() {
+    return ENTRY_SIZE_IN_BITS_FOR_CONSTANT_POOL_MAP;
+  }
+
+  public static int entrySizeInBitsForApiLevelMap() {
+    return ENTRY_SIZE_IN_BITS_FOR_API_MAP;
+  }
+
+  public static int apiLevelHash(DexReference reference) {
+    int entrySize = entrySizeInBitsForApiLevelMap();
+    int size = 1 << (entrySize - 1);
+    return (reference.hashCode() % size) + size;
+  }
+
+  public static int constantPoolHash(DexString string) {
+    int entrySize = entrySizeInBitsForConstantPoolMap();
+    int size = 1 << (entrySize - 1);
+    return (string.hashCode() % size) + size;
+  }
+
+  static int constantPoolEntrySize() {
+    return PAYLOAD_OFFSET_WITH_LENGTH;
+  }
+
+  static int constantPoolMapEntrySize() {
+    return PAYLOAD_OFFSET_WITH_LENGTH;
+  }
+
+  static int apiLevelHashMapEntrySize() {
+    return PAYLOAD_OFFSET_WITH_LENGTH;
+  }
+
+  static int constantPoolOffset() {
+    return 4;
+  }
+
+  static int constantPoolHashMapOffset(int constantPoolSize) {
+    return (constantPoolSize * constantPoolEntrySize()) + constantPoolOffset();
+  }
+
+  static int apiLevelHashMapOffset(int constantPoolSize) {
+    int constantPoolHashMapSize =
+        (1 << entrySizeInBitsForConstantPoolMap()) * constantPoolMapEntrySize();
+    return constantPoolHashMapOffset(constantPoolSize) + constantPoolHashMapSize;
+  }
+
+  static int payloadOffset(int constantPoolSize) {
+    int apiLevelSize = (1 << entrySizeInBitsForApiLevelMap()) * apiLevelHashMapEntrySize();
+    return apiLevelHashMapOffset(constantPoolSize) + apiLevelSize;
+  }
+
+  static int readIntFromOffset(byte[] data, int offset) {
+    return Ints.fromBytes(data[offset], data[offset + 1], data[offset + 2], data[offset + 3]);
+  }
+
+  static int readShortFromOffset(byte[] data, int offset) {
+    return Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, data[offset], data[offset + 1]);
+  }
+
+  private int constantPoolSizeCache = -1;
+
+  abstract int readConstantPoolSize();
+
+  abstract PositionAndLength getConstantPoolPayloadOffset(int index);
+
+  abstract PositionAndLength getConstantPoolHashMapPayloadOffset(int hash);
+
+  abstract PositionAndLength getApiLevelHashMapPayloadOffset(int hash);
+
+  abstract boolean payloadHasConstantPoolValue(int offset, int length, byte[] value);
+
+  abstract int payloadContainsConstantPoolValue(
+      int offset, int length, byte[] value, BiPredicate<Integer, byte[]> predicate);
+
+  abstract byte readApiLevelForPayloadOffset(int offset, int length, byte[] value);
+
+  public int getConstantPoolSize() {
+    if (constantPoolSizeCache == -1) {
+      constantPoolSizeCache = readConstantPoolSize();
+    }
+    return constantPoolSizeCache;
+  }
+
+  /** When the first bit is set (position < 0) then there is a single unique result for the hash. */
+  public static boolean isUniqueConstantPoolEntry(int position) {
+    return position < 0;
+  }
+
+  /**
+   * If the position defines a unique result, the first byte is has the first bit set to 1 (making
+   * it negative) and the actual index specified in the least significant two bytes.
+   */
+  public static int getConstantPoolIndexFromUniqueConstantPoolEntry(int position) {
+    assert isUniqueConstantPoolEntry(position);
+    return unsetBitAtIndex(position, 32);
+  }
+
+  public int getConstantPoolIndex(DexString string) {
+    PositionAndLength constantPoolIndex =
+        getConstantPoolHashMapPayloadOffset(constantPoolHash(string));
+    if (constantPoolIndex.isEmpty()) {
+      return -1;
+    }
+    int position = constantPoolIndex.getPosition();
+    int length = constantPoolIndex.getLength();
+    if (isUniqueConstantPoolEntry(position)) {
+      int nonTaggedPosition = getConstantPoolIndexFromUniqueConstantPoolEntry(position);
+      if (isConstantPoolEntry(nonTaggedPosition, string.content)) {
+        return nonTaggedPosition;
+      }
+    } else {
+      assert length > 0;
+      return payloadContainsConstantPoolValue(
+          position, length, string.content, this::isConstantPoolEntry);
+    }
+    return -1;
+  }
+
+  public boolean isConstantPoolEntry(int index, byte[] value) {
+    PositionAndLength constantPoolPayloadOffset = getConstantPoolPayloadOffset(index);
+    if (constantPoolPayloadOffset.isEmpty()) {
+      return false;
+    }
+    return payloadHasConstantPoolValue(
+        constantPoolPayloadOffset.getPosition(), constantPoolPayloadOffset.getLength(), value);
+  }
+
+  public byte getApiLevelForReference(byte[] serialized, DexReference reference) {
+    PositionAndLength apiLevelPayloadOffset =
+        getApiLevelHashMapPayloadOffset(apiLevelHash(reference));
+    if (apiLevelPayloadOffset.isEmpty()) {
+      return 0;
+    }
+    return readApiLevelForPayloadOffset(
+        apiLevelPayloadOffset.getPosition(), apiLevelPayloadOffset.getLength(), serialized);
+  }
+
+  public static byte findApiForReferenceHelper(byte[] data, int offset, int length, byte[] value) {
+    int index = offset;
+    while (index < offset + length) {
+      // Read size of entry
+      int lengthOfEntry = Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, data[index], data[index + 1]);
+      int startIndex = index + 2;
+      int endIndex = startIndex + lengthOfEntry;
+      if (isSerializedDescriptor(value, data, startIndex, lengthOfEntry)) {
+        return data[endIndex];
+      }
+      index = endIndex + 1;
+    }
+    return 0;
+  }
+
+  protected static boolean isSerializedDescriptor(
+      byte[] serialized, byte[] candidate, int offset, int length) {
+    if (serialized.length != length) {
+      return false;
+    }
+    for (int i = 0; i < length; i++) {
+      if (serialized[i] != candidate[i + offset]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public static class AndroidApiDataAccessInMemory extends AndroidApiDataAccess {
+
+    private final byte[] data;
+
+    private AndroidApiDataAccessInMemory(byte[] data) {
+      this.data = data;
+    }
+
+    public static AndroidApiDataAccessInMemory create() {
+      byte[] data;
+      try (InputStream apiInputStream =
+          AndroidApiDataAccess.class.getClassLoader().getResourceAsStream(RESOURCE_NAME); ) {
+        if (apiInputStream == null) {
+          URL resource = AndroidApiDataAccess.class.getClassLoader().getResource(RESOURCE_NAME);
+          throw new CompilationError("Could not find the api database at: " + resource);
+        }
+        data = ByteStreams.toByteArray(apiInputStream);
+      } catch (IOException e) {
+        throw new CompilationError("Could not read the api database.", e);
+      }
+      return new AndroidApiDataAccessInMemory(data);
+    }
+
+    @Override
+    public int readConstantPoolSize() {
+      return readIntFromOffset(data, 0);
+    }
+
+    @Override
+    PositionAndLength getConstantPoolPayloadOffset(int index) {
+      int offset = constantPoolOffset() + (index * constantPoolEntrySize());
+      return PositionAndLength.create(data, offset);
+    }
+
+    @Override
+    PositionAndLength getConstantPoolHashMapPayloadOffset(int hash) {
+      int offset =
+          constantPoolHashMapOffset(getConstantPoolSize()) + (hash * constantPoolMapEntrySize());
+      return PositionAndLength.create(data, offset);
+    }
+
+    @Override
+    PositionAndLength getApiLevelHashMapPayloadOffset(int hash) {
+      int offset =
+          apiLevelHashMapOffset(getConstantPoolSize()) + (hash * apiLevelHashMapEntrySize());
+      return PositionAndLength.create(data, offset);
+    }
+
+    @Override
+    boolean payloadHasConstantPoolValue(int offset, int length, byte[] value) {
+      return isSerializedDescriptor(
+          value, data, payloadOffset(getConstantPoolSize()) + offset, length);
+    }
+
+    @Override
+    int payloadContainsConstantPoolValue(
+        int offset, int length, byte[] value, BiPredicate<Integer, byte[]> predicate) {
+      int payloadOffset = payloadOffset(getConstantPoolSize());
+      int startInPayload = payloadOffset + offset;
+      int endInPayload = startInPayload + length;
+      if (data.length < endInPayload) {
+        return -1;
+      }
+      for (int i = startInPayload; i < endInPayload; i += 2) {
+        int index = Ints.fromBytes(ZERO_BYTE, ZERO_BYTE, data[i], data[i + 1]);
+        if (predicate.test(index, value)) {
+          return index;
+        }
+      }
+      return -1;
+    }
+
+    @Override
+    byte readApiLevelForPayloadOffset(int offset, int length, byte[] value) {
+      return findApiForReferenceHelper(
+          data, payloadOffset(getConstantPoolSize()) + offset, length, value);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java
index f5343da..fdffd69 100644
--- a/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java
+++ b/src/main/java/com/android/tools/r8/androidapi/AndroidApiLevelHashingDatabaseImpl.java
@@ -4,109 +4,154 @@
 
 package com.android.tools.r8.androidapi;
 
+import static com.android.tools.r8.lightir.ByteUtils.isU2;
 import static com.android.tools.r8.utils.AndroidApiLevel.ANDROID_PLATFORM;
 
-import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.androidapi.AndroidApiDataAccess.AndroidApiDataAccessInMemory;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.android.tools.r8.utils.structural.DefaultHashingVisitor;
-import com.android.tools.r8.utils.structural.HasherWrapper;
-import com.android.tools.r8.utils.structural.StructuralItem;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
-import java.io.BufferedReader;
+import com.android.tools.r8.utils.ThrowingFunction;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.ObjectInputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.stream.Collectors;
 
 public class AndroidApiLevelHashingDatabaseImpl implements AndroidApiLevelDatabase {
 
-  public static HasherWrapper getDefaultHasher() {
-    return HasherWrapper.murmur3128Hasher();
+  private static final byte TYPE_IDENTIFIER = 0;
+  private static final byte FIELD_IDENTIFIER = 1;
+  private static final byte METHOD_IDENTIFIER = 2;
+
+  private static final byte[] NON_EXISTING_DESCRIPTOR = new byte[0];
+
+  public static byte[] getNonExistingDescriptor() {
+    return NON_EXISTING_DESCRIPTOR;
   }
 
-  private final Int2ReferenceMap<AndroidApiLevel> lookupNonAmbiguousCache =
-      new Int2ReferenceOpenHashMap<AndroidApiLevel>();
-  private final Map<String, AndroidApiLevel> ambiguousHashesWithApiLevel = new HashMap<>();
-  private final Map<DexReference, AndroidApiLevel> ambiguousCache = new IdentityHashMap<>();
+  public static byte[] getUniqueDescriptorForReference(
+      DexReference reference, ThrowingFunction<DexString, Integer, IOException> constantPoolLookup)
+      throws IOException {
+    if (reference.isDexType()) {
+      return typeToBytes(constantPoolLookup.apply(reference.asDexType().getDescriptor()));
+    }
+    int holderId =
+        constantPoolLookup.apply(reference.asDexMember().getHolderType().getDescriptor());
+    if (holderId < 0) {
+      return NON_EXISTING_DESCRIPTOR;
+    }
+    int nameId = constantPoolLookup.apply(reference.asDexMember().getName());
+    if (nameId < 0) {
+      return NON_EXISTING_DESCRIPTOR;
+    }
+    if (reference.isDexField()) {
+      return fieldToBytes(
+          holderId,
+          nameId,
+          constantPoolLookup.apply(reference.asDexField().getType().getDescriptor()));
+    }
+    assert reference.isDexMethod();
+    return methodToBytes(holderId, nameId, reference.asDexMethod(), constantPoolLookup);
+  }
+
+  private static byte[] typeToBytes(int typeId) {
+    if (typeId < 0) {
+      return NON_EXISTING_DESCRIPTOR;
+    }
+    return new byte[] {
+      TYPE_IDENTIFIER, getFirstByteFromShort(typeId), getSecondByteFromShort(typeId)
+    };
+  }
+
+  private static byte[] fieldToBytes(int holderId, int nameId, int typeId) {
+    if (holderId < 0 || nameId < 0 || typeId < 0) {
+      return NON_EXISTING_DESCRIPTOR;
+    }
+    return new byte[] {
+      FIELD_IDENTIFIER,
+      getFirstByteFromShort(holderId),
+      getSecondByteFromShort(holderId),
+      getFirstByteFromShort(nameId),
+      getSecondByteFromShort(nameId),
+      getFirstByteFromShort(typeId),
+      getSecondByteFromShort(typeId)
+    };
+  }
+
+  private static byte[] methodToBytes(
+      int holderId,
+      int nameId,
+      DexMethod method,
+      ThrowingFunction<DexString, Integer, IOException> constantPoolLookup)
+      throws IOException {
+    if (holderId < 0 || nameId < 0) {
+      return NON_EXISTING_DESCRIPTOR;
+    }
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    baos.write(METHOD_IDENTIFIER);
+    baos.write(getFirstByteFromShort(holderId));
+    baos.write(getSecondByteFromShort(holderId));
+    baos.write(getFirstByteFromShort(nameId));
+    baos.write(getSecondByteFromShort(nameId));
+    for (DexType parameter : method.proto.parameters) {
+      int parameterId = constantPoolLookup.apply(parameter.getDescriptor());
+      if (parameterId < 0) {
+        return NON_EXISTING_DESCRIPTOR;
+      }
+      baos.write(getFirstByteFromShort(parameterId));
+      baos.write(getSecondByteFromShort(parameterId));
+    }
+    int returnTypeId = constantPoolLookup.apply(method.getReturnType().getDescriptor());
+    if (returnTypeId < 0) {
+      return NON_EXISTING_DESCRIPTOR;
+    }
+    baos.write(getFirstByteFromShort(returnTypeId));
+    baos.write(getSecondByteFromShort(returnTypeId));
+    return baos.toByteArray();
+  }
+
+  private static byte getFirstByteFromShort(int value) {
+    assert isU2(value);
+    return (byte) (value >> 8);
+  }
+
+  private static byte getSecondByteFromShort(int value) {
+    assert isU2(value);
+    return (byte) value;
+  }
+
+  private final Map<DexReference, AndroidApiLevel> lookupCache = new IdentityHashMap<>();
+  private final Map<DexString, Integer> constantPoolCache = new IdentityHashMap<>();
+  private static volatile AndroidApiDataAccess dataAccess;
+
+  private static AndroidApiDataAccess getDataAccess() {
+    if (dataAccess == null) {
+      synchronized (AndroidApiDataAccess.class) {
+        if (dataAccess == null) {
+          dataAccess = AndroidApiDataAccessInMemory.create();
+        }
+      }
+    }
+    return dataAccess;
+  }
 
   public AndroidApiLevelHashingDatabaseImpl(
       List<AndroidApiForHashingReference> predefinedApiTypeLookup) {
-    loadData();
     predefinedApiTypeLookup.forEach(
         predefinedApiReference -> {
-          int hashCode = predefinedApiReference.getReference().hashCode();
           // Do not use computeIfAbsent since a return value of null implies the key should not be
           // inserted.
-          if (!lookupNonAmbiguousCache.containsKey(hashCode)) {
-            lookupNonAmbiguousCache.put(hashCode, null);
-            ambiguousCache.put(
-                predefinedApiReference.getReference(), predefinedApiReference.getApiLevel());
-          }
+          lookupCache.put(
+              predefinedApiReference.getReference(), predefinedApiReference.getApiLevel());
         });
     assert predefinedApiTypeLookup.stream()
         .allMatch(added -> added.getApiLevel().isEqualTo(lookupApiLevel(added.getReference())));
   }
 
-  private void loadData() {
-    int[] hashIndices;
-    byte[] apiLevels;
-    List<String> ambiguous;
-    try (InputStream indicesInputStream =
-            getClass()
-                .getClassLoader()
-                .getResourceAsStream("api_database/api_database_hash_lookup.ser");
-        ObjectInputStream indicesObjectStream = new ObjectInputStream(indicesInputStream);
-        InputStream apiInputStream =
-            getClass()
-                .getClassLoader()
-                .getResourceAsStream("api_database/api_database_api_level.ser");
-        ObjectInputStream apiObjectStream = new ObjectInputStream(apiInputStream);
-        InputStream ambiguousInputStream =
-            getClass()
-                .getClassLoader()
-                .getResourceAsStream("api_database/api_database_ambiguous.txt")) {
-      hashIndices = (int[]) indicesObjectStream.readObject();
-      apiLevels = (byte[]) apiObjectStream.readObject();
-      ambiguous =
-          new BufferedReader(new InputStreamReader(ambiguousInputStream, StandardCharsets.UTF_8))
-              .lines()
-              .collect(Collectors.toList());
-    } catch (IOException | ClassNotFoundException e) {
-      throw new RuntimeException("Could not build api database");
-    }
-    assert hashIndices.length == apiLevels.length;
-    for (int i = 0; i < hashIndices.length; i++) {
-      byte apiLevel = apiLevels[i];
-      lookupNonAmbiguousCache.put(
-          hashIndices[i], apiLevel == -1 ? null : AndroidApiLevel.getAndroidApiLevel(apiLevel));
-    }
-    ambiguous.forEach(this::parseAmbiguous);
-  }
-
-  /**
-   * All elements in the ambiguous map are on the form <key>:<api-level>. The reason for this
-   * additional map is that the keys collide for the items using the ordinary hashing function.
-   */
-  private void parseAmbiguous(String ambiguous) {
-    String[] split = ambiguous.split(":");
-    if (split.length != 2) {
-      throw new CompilationError("Expected two entries in ambiguous map");
-    }
-    ambiguousHashesWithApiLevel.put(
-        split[0], AndroidApiLevel.getAndroidApiLevel(Integer.parseInt(split[1])));
-  }
-
   @Override
   public AndroidApiLevel getTypeApiLevel(DexType type) {
     return lookupApiLevel(type);
@@ -122,34 +167,35 @@
     return lookupApiLevel(field);
   }
 
+  private int getConstantPoolId(DexString string) {
+    return constantPoolCache.computeIfAbsent(
+        string, key -> getDataAccess().getConstantPoolIndex(string));
+  }
+
   private AndroidApiLevel lookupApiLevel(DexReference reference) {
     // We use Android platform to track if an element is unknown since no occurrences of that api
     // level exists in the database.
-    AndroidApiLevel result =
-        lookupNonAmbiguousCache.getOrDefault(reference.hashCode(), ANDROID_PLATFORM);
-    if (result != null) {
-      return result == ANDROID_PLATFORM ? null : result;
+    AndroidApiLevel result = lookupCache.get(reference);
+    if (result == null) {
+      byte[] uniqueDescriptorForReference;
+      try {
+        uniqueDescriptorForReference =
+            getUniqueDescriptorForReference(reference, this::getConstantPoolId);
+      } catch (Exception e) {
+        uniqueDescriptorForReference = getNonExistingDescriptor();
+      }
+      if (uniqueDescriptorForReference == getNonExistingDescriptor()) {
+        result = ANDROID_PLATFORM;
+      } else {
+        byte apiLevelForReference =
+            getDataAccess().getApiLevelForReference(uniqueDescriptorForReference, reference);
+        result =
+            (apiLevelForReference <= 0)
+                ? ANDROID_PLATFORM
+                : AndroidApiLevel.getAndroidApiLevel(apiLevelForReference);
+      }
+      lookupCache.put(reference, result);
     }
-    return ambiguousCache.computeIfAbsent(
-        reference,
-        ignored -> {
-          HasherWrapper defaultHasher = getDefaultHasher();
-          reference.accept(
-              type -> DefaultHashingVisitor.run(type, defaultHasher, DexType::acceptHashing),
-              field ->
-                  DefaultHashingVisitor.run(field, defaultHasher, StructuralItem::acceptHashing),
-              method ->
-                  DefaultHashingVisitor.run(method, defaultHasher, StructuralItem::acceptHashing));
-          String existingHash = defaultHasher.hash().toString();
-          AndroidApiLevel androidApiLevel = ambiguousHashesWithApiLevel.get(existingHash);
-          if (androidApiLevel == null) {
-            throw new CompilationError(
-                "Failed to find api level for reference: "
-                    + reference.toSourceString()
-                    + " with hash value: "
-                    + existingHash);
-          }
-          return androidApiLevel;
-        });
+    return result == ANDROID_PLATFORM ? null : result;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/lightir/ByteUtils.java b/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
index 40a80d2..5d6d5a7 100644
--- a/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
+++ b/src/main/java/com/android/tools/r8/lightir/ByteUtils.java
@@ -35,4 +35,16 @@
     writer.put(truncateToU1(value >> 8));
     writer.put(truncateToU1(value));
   }
+
+  public static boolean isU2(int value) {
+    return value >= 0 && value <= 0xFFFF;
+  }
+
+  public static int unsetBitAtIndex(int value, int index) {
+    return value & ~(1 << index - 1);
+  }
+
+  public static int setBitAtIndex(int value, int index) {
+    return value | (1 << index - 1);
+  }
 }
diff --git a/src/main/resources/api_database/api_database_ambiguous.txt b/src/main/resources/api_database/api_database_ambiguous.txt
deleted file mode 100644
index 47dfde9..0000000
--- a/src/main/resources/api_database/api_database_ambiguous.txt
+++ /dev/null
@@ -1,473 +0,0 @@
-d8c43560e159bcb899c2180b8e01d8e3:24
-ae9c7aee12dfaaa56e43834dcea91f5f:24
-106252e3f31ee17239e1ee4184147deb:24
-12233f83b06081671815562dce938d3b:24
-1fb2cd38813bad6e5eeef74bcbcb133d:24
-c5bbd4174eee5cb68830801317071dd5:24
-b876313caea388f03200ac720ed528af:24
-f4fa53a97c83899fd2da9f4112f35339:24
-dc32c09ecdd9bda4703aa944cf1eb4ca:24
-07a59dae4166f6d504bb0793c5e75178:24
-efcb5024ce3514453d44735222903a55:24
-43445dc9e50e2c4fc327097d07ff749a:24
-bcb1787dd2def454fd3f31d1b2c0967a:24
-cace738770550444ee1f91000c32c147:24
-2cc65559141cece33ffdbc4e34a2ce3b:24
-b55a09768f05f29b15e1ff3a9feb7c92:24
-54282406a85f4410d4dfc72d5873ee96:24
-1c38913c447705e315ec4486da558c9d:24
-ca3f30645fa7b480b213f4228386a09a:24
-6d8800c5cc8ad9e4f3f28e777207a870:24
-b8dc6f9b9587159f978d643aeb8e4d07:24
-44c0d27dc28d4da28655c3c62a0d7cac:24
-91eb697571f68f5850875286c5e36080:24
-ae908f7abd7d347b0bf39fead3531c12:24
-145e730e204fdca9cf169521a3812ed1:24
-d27ceaaa92270c450aeee582cc17b985:24
-294141fdc8f17ac3c7e4d8687d8ad389:24
-48de820418038280f70eca64f878c67b:24
-cfde9ea95c663bed6d56df8778b01b79:24
-8569110aa82eb44ff0b12819867c66f8:24
-f389ac23dc4ef6641321adf43d712c32:24
-46de27224f4cf6fb745e30bbd6aad267:24
-ef4661e0074b431762177e10c2ac8570:24
-75c79913d4df6b467a67c40ce67a32fc:24
-2a000aca1e17b3b0343ddf151ee313f4:24
-e9d59e0325c6dd1dd78a998365435fac:24
-0a1d88442fc34d0081115d949f1c5fa9:24
-0a7ef5f8b75fa2dd79787b03dff923c9:24
-63f61697ff1319a76632338fe0c13ae9:24
-b247285b58ca44aad532b1c3b5a5dacf:24
-e8df9edc7e29252645d2b8a6a55c4e0f:24
-aaba729f45b1617d59cec26aa41937e3:24
-7e7ae6228d4ab54b90f0f580731941d7:24
-de1c84862deb139962240505133690f0:24
-9660da0a81f885de5a3b16f1f07a1067:24
-9dbabf8883b7a4e120fcbceb00face40:24
-c3f5202b30c04801da693013368a0da2:24
-b73113e1f7c2702cfd997dd84e5ac003:24
-9b5946e54278b49302da6ccd2a3a06e1:24
-c7a5a5244ef8d55b160e29df7e310b76:24
-74fa889a011613ee983f45aed7dcb656:24
-ccb57b751fd0ed3f7244a0e096b19076:24
-dcd61dd6bd1e0fec6888029656162559:24
-c2fa9ac71ea25473f3c384e5d3c3770b:24
-43de03bf819ac106a9a66f3d7b473bd1:24
-3ed4b5f668130ecec1aadb21bf0a671a:24
-ba4d4fc3c411bf5b477323a1329574b3:24
-4e82fa0797beeab61a7a0b9b3ba7505e:24
-1cff05a0b4291e8f289a1794f016eab5:24
-456c3251cceb566a82d3175390b6a663:24
-11e854ed754739797dbae75ff3159e82:24
-2daef0ca0e135ad904bf7e931690cf2b:24
-76fd080fdaa80390ca05399f4d0b49ae:24
-d49bee1da6282ff1efd4017e2c26ff55:24
-94bf636e74064a1f45c5e81777a8b541:24
-b502fdd440592e2d637704ebef9baf01:24
-67395fd629dae7b237421aa91bc12150:24
-b69dd3d2f26fc766f3b4193b089fa71e:24
-5a13a7fac55b3378629744099623199b:24
-a9446b05afd48f274f6469e9617dadde:24
-ef1a937ed3ed1277a46cbbfb589bcc7c:24
-6c21462d7f8529e6d017e054b02d96d3:24
-03b1c2c2b9e04b66ace80c8d891dad71:24
-b4146bc53ba79957adf0bf7306b2212c:24
-5af3ef227d419594dc0316392c0fd762:24
-f1136465059f10586c4fb8379126e010:24
-8796edfd2200b66560e180ecf2c8bd3a:24
-b63216a8584c12bf002f4ee8c3b0ac3e:19
-1ad1cfa76f9692958656f17b710a0ecc:19
-828623182530e5b5c55265b6a20263be:9
-b36bec8fd49158c020b6d8c09cc02baf:26
-e7e718f70ee49560800db2d60270603f:11
-61c32de62fbec72e56083dade766f0f9:11
-f9684960dbadc5206f33484afeba57e1:11
-f849d1e2b1fbe25e1c88aa103ce68255:11
-1186fd5fd194d7f5c3c1b19aa753d315:11
-c74e6a2c593956c2e74a9f89dac73eeb:11
-e528c2b6258e260f575488c35f7683fb:11
-e5c61babf2a6ff8f6939b7a1db7e621f:11
-48921ae81d5ad84778fd75c9f58e1bf9:11
-92e7e194987c3b4db1a4a129af2cd05f:11
-f1d93cdeba77ba27e048fd0fbc6b0bad:11
-2cb543910592d445cf09fec159d0b421:11
-cd3bfb7c471b99eb7b7acb8011e4687f:11
-df936e8a13b49cdb9b5f82e150aca29b:11
-0e452471f905f2fb311c6146b31b5705:11
-b78f4f3edd36b6326873eea475ef2476:11
-fa0bc15e6c1bcb787bdea1073a4e2709:11
-417ebe49420a8d103b8b65c5d574fd84:11
-18f7890363dbd6262668cdcffb0f3a8d:11
-28c73ab584871ffb89a5304f87335f95:11
-aa4a8bfad0ec9e57489297bc67664cfd:11
-9a5dd5c667e21679c42fc7b2cd79769c:11
-59654e2c6df1b6a8a77928223e2ab332:11
-fea47d75f1c8cdb3a12de27678c005b6:11
-c6dd561601e904cee3707ba4cfe1350f:11
-02ceeb351445bd756a73fd5ab2a3d0c4:11
-3a3e699a3fdaaaf0b0d809d114c54e19:11
-ccad7981fb017550a8f7b8925a1d0648:11
-0803d11e374ba9493cfdd8fc17f7d34c:11
-31b7ca8caafc4ac8fd6ab18a01f5e9f5:11
-762dbaa4b1beab4b7113a750639d7f6f:11
-cf1b9fa0beb54f8f57c79446a3186baa:11
-13c8db7cad8e47068720c9f9b9d8213c:11
-d942884e00b9f288de96a8b0733372d9:11
-cd5c01104e0c46080a8d499f87de4991:11
-799e6e8b1fe3f2c45ec9703491bd67d5:8
-12011f7bb75e99008d3ef5bbe9087e6f:8
-53737c53af7f40961557a85fb4d11b55:8
-9fac9097934ab48cd9de3d124943c3fd:8
-a85598686e913ff9a84c078990e2b74f:8
-9389bbac5d37c3655c2d112b5fb15d79:8
-c7143cf932c4620b38f241a67c698de7:8
-d5360a8c6fd960a52c125ad6ee65b369:8
-6bb42f291857bf93ef2407e3fe8910b3:8
-4c0513c3ea8aa4e0d1671697ade55dc0:8
-9ab999023c47266804e1c23912c49590:8
-c83ba14b99739545b5611680823c5e64:8
-6c3b66ed41f23693ee6ed1a2f74383ca:8
-e064938f6319932b4685797aa810233f:8
-edf457cbc465fa576479f66ed5689a8e:8
-befe448f726cd55c55d708a8ed55b087:8
-4a35639e3583bbc1b36b56f0b50a2849:8
-a2c469af732e034c5017e1fbcbb6fba0:8
-47ef9cb8fb8fa55cfe73612c56fc5a0d:8
-58e29b4d0c775a250e2751d3cb70e44b:8
-2248103eacf690b2060d9f624dee5e41:8
-7aaff576c6f4f28ee8d80236c3943c3a:8
-850665aac5116ea7c5dd3ed4d672cc68:1
-d853b59bd420a046a715750d924360d0:1
-1a9e947630aeed44da4b6e668e8ac214:1
-d96d3dfd316fb498b8989986135bb848:1
-281b8e41d83f2962772157285ee84c38:1
-943b393cd0aa3d44e2609c7a7008e36f:1
-b1f7571f9f4648ce69a9e79455d3df8a:1
-22def9631f444a8421104a4c6fd26845:1
-d448211aa9d792c542c8a12499062780:1
-843e12eb3c7a2c2c7c2d1b64058d619e:1
-6fa6dc2897ec7eaec581306eb1cbacae:1
-1b6414136749889f64248ad232774cd4:1
-aeb599339c06ae54c794c71be6b2b1f0:1
-9a57214d87842677a74f893161a4b9be:1
-aa69312c004e15f4fa8efce471b0cfc1:1
-619d04422f81707ae4354cb9c8b41ed5:1
-8fe152aee223813be38a1103ba72c66e:1
-e19b30d13798c227510c8fb9d5ca20ec:1
-c892a71c9b51dff23b4b6d4d78c46ca0:1
-97d4f84cdaf7386e47d1c8bea51a45b5:1
-e1ed03e2c2a9fd3a5866fb3771530dc9:1
-2efc71cc840f54a63d8ce5ff313a7e2f:1
-e367ef6e11b586255cbde00200814031:1
-e1863e976cd3e82c00e43304db807943:1
-3fb35789d4aee6489350c670908bd59e:1
-e63feec2ab98295f4cd1f09d21e35ce9:1
-f081ee3e1cb0dec48d0127716393edec:1
-1db593f23925e8dc2fb326b2a368335c:1
-089be95001c81698ed7a1ae6eadb480b:1
-62e65d09c285da8da8dbecff2a85eb5e:1
-466714572719ed5745a5b5cd886c880b:1
-080573d2f4292d0789f942bd39af7594:1
-449e3c63d3565b07e6e39ae06b36e9a1:1
-96ca46ef9f7f9ed890508a01899764eb:1
-603e49aeac3c895300c860d6bd596135:1
-0de08429c1f0d8c9804107e8dac151f0:1
-aba1819af7abfe17006adcf74c572eab:1
-a1061d47d7a301608f4f7b396bb81ee3:1
-cf60514a6e7b90495f5a47161478a33f:1
-e8890acb1c71aa0076dffdb89dff32e3:1
-987a12e7a344e556d9544c953d62ae8b:1
-8f28909290b786920a4b9fb635ba6677:1
-a0d0240922deab6b46a7ba1509aa3168:1
-d9eb1dbdb92903db5b787e2e70e34c39:1
-9c45ce862efc619f1cca962c8caeb844:1
-0ef069b7a04f779edb4eddc5d9e68664:1
-e04bb82da6212e6b9993e2f2177d7b34:1
-e97bced61f81e5e6486cabccafd486b9:1
-c2d6153e0b11fa485a1db9b895d7ca4f:1
-4a3f92803a9af2349140ee2d20d4016f:1
-45c104bb4202da85198b615491d1de23:1
-c841eae393bc4c731d85b057b263effa:1
-641c008d5e49914df8db013a5bb050e7:1
-00217564c415ce5c03abf66b6863f30a:1
-a98b9be4a37764162034e849091e2f2a:1
-7275881c062d5fa141b8beef6d60f05b:1
-36779dd0e73608472e1c22fca2914eba:1
-ddda7f2d9e16b789b36d9235d1a4b52a:1
-1e39ef60367e236fa336f96705b2b10f:1
-5b17c7c02bc7ab47a30ea2856738af8c:1
-9963627e831a6315767f99893ca660c1:1
-43baf0e2fd2a825c22e70c4f589441df:1
-eceb74f53e3044ee33a39ed0b6c9cc73:1
-769d9f4e5046a4e0a480b614eb530cb1:1
-844cdb718520e79a957bd880c6e046b5:1
-f25a0a19ee375dbe75922bf1817f37de:1
-fe40c696eb0567e090d8968b387100e4:1
-885d4879046a451b2330409a9c316a86:1
-ebb652ecc8bc2f4a2d616a8294624e1d:1
-60c0494c6547c2aa37081b0a045f2dbb:1
-d97969716904e1a32e4ce6c4de736fac:1
-c12acdd4d8e17d803e32eb32757be4e9:1
-83255b102f7327ffe0819cc9d48d764a:1
-a159e58a81a5d7e46b1d44f845486c25:1
-c9a7fe526ed238e56f029bdd9eadd4a8:1
-beef3d82791567a66852903d9acde2d8:14
-8f8e727dba622a81127060f53efbaf60:14
-a58ec4e669821ad885c5e539bea8f8c0:14
-c183aebc8596eae7dbaf32f831d4bf1a:14
-579a40bd08705acc7099b631706917cd:14
-c979623104ac1df2ff0b56f2eb8f40a3:14
-4a406313d3d1fc7c56f2e4ae88033ef5:14
-0534bb586552faf59923fcbb66c5c9a8:14
-ebf47efadc4c50002b13da0b1cef0646:18
-106de6ca4d0e49988f3328ce380ff517:18
-93aca156e3f15799914f868852723e47:18
-2f4aedd17528c30f8a9f16b030ff708b:18
-17cbc97604a781acb78c668b2437ffbd:18
-1709449e191fe131236bd0b8c1928b71:18
-a6cf882fc575147513b7b1b505fdd335:18
-1587f690fd5c0c886630334e89e397ff:18
-dc6ad4bf6892204b9fca73d1ee9cab04:18
-9ae673cfbb5d90688edafbf7ec2862e4:18
-acf250fcdbf54725219af8c6a8a614d9:18
-77e978f6318d01be894de8944fdc05b4:18
-07319a58381faa784dda046d5e8fd8ae:18
-81be9a78d26d25c6ff47c7e9af30a92e:18
-e87bf9d0084b39c443e421cdb5d3fc43:18
-9e973ee7f4f5ad967d2ba5bf5d88911c:18
-b47e7bbf50ad208599056f8d0f1cc5ba:18
-47a9e25289b4ece230adcc3b60797712:18
-42e4cc1882d334757afbe74f4d8cec8e:18
-024481748991b858bf7e03b0b87ce125:18
-b6ced7e04b527d83cd660894a0059ea7:18
-717e536c3a13f3602fbaf1f607ee2963:18
-f1483fe126d64bd7544dba7fdf0559f4:18
-18b540e31006aad6ae7a26d967ec0e0e:18
-3c4a0d8270a52e43316a711a2fe83268:18
-2e13bb1bfba423473a6efba65d9c8190:18
-47a5d9010bdeb561176cf070ef483e91:18
-4f2e7d32ed2c7ead316770fbe84d8aae:18
-c625e1090fc4d72afd0673839af5d46f:18
-780c7e0f80b5479d00e2c3679974644d:18
-eff51a62305642c720d163085061ed63:18
-4cc2dc623ee702db7a5899c5c401a98c:18
-69e416f1afc250d6239788b4b1fb9bb4:18
-d71a32697989404ae782e105a5eb5e4c:18
-e16e8dcca650cf79557cb76a86edb4e9:18
-6d60d78c1ce897131dcf45743a8d6e49:18
-bfebe4871ed9c4b5f3300fe8dfa2a0a3:18
-93e215f230901f20bdca460cfe8c42af:18
-8efacada80287ebb7607758a0a82b0a0:18
-487bc1228fd817b641056a1a008984f8:18
-dc6c2915cd6f0a6fd0ab01365349599d:18
-1a3dbf72e4bd135a621b8ad78a7151c1:18
-83c42b475763d64935a9ddfc6a374b64:18
-01fd60d48ba7e1434bb72696282c7113:18
-eeb05b41976f7bb96572330854aaab34:18
-ed9a26aaf7ddb748bd1e624aeb4fdb81:18
-3c17437ba337e7ce8edad092f745a363:18
-8d8fd1ffc3ae24cb43798f4eb41fc4de:18
-003f7ab7088710994a289bab815aacbf:18
-cab779407e29ba3377e6a47a4f842ec5:18
-488d943cdcedfca2b076152769fc3c51:18
-a0ddcf0f7bb4aa9d38db983941f6908d:18
-b64b41d26010783cae2dff88c7e21f1d:18
-aac6d054ad0f9bea1f64702aea698ee3:18
-90b4163293f9e35f7091692b31d3b5d7:18
-9d66fceb1054d5e2ff207591ec216b70:18
-93a65566b576af220207e30cc89ef490:18
-b3248e6a2d819bd8bb42a15179188c52:18
-f95f0545feb040d2d6bfcad6897364d0:18
-058ec3044e95ed7fcaff93a2f3105d6f:18
-39ef3516cb80752cac1c541b43a1c2fa:18
-c1295d79d79340152c6b98f1c9003e8c:18
-c4b4a2994711cb342b0c8c8ef409ce8b:18
-719c3b73816fa0db79747ea264abab5a:18
-1211e656a8eaa4423e7678533a1e7e2a:18
-1d7847b88e0feaecd299b3c5c18be763:18
-e0c7417a74df21b55367f9e1ea6c2d37:18
-e49b0a2bd830bb3b96934490b2bc6ebe:18
-91790d7c5cf146ab3936919184a70015:18
-ec36ea4bb9634f7ec5c4d3c1cc9dd71b:18
-458fda8d9863be24862bb62c910f427d:18
-860e7b1a68c02eef7d2e2d7b6a3e13b5:18
-e05ed0452c12e62ef74e9161159b1231:18
-9419764761c059b9c2f9cf640abfdeb0:18
-ea02175c4ef64b6370f619acd84f3c3c:18
-0b01d40e1ca37aa854e77c98cc153e77:18
-eb12f3e690b201aa171d794a8288af10:18
-b955e5418d490425f99d58f441ba4525:18
-62068b685c80a1da76ef1824af304e47:5
-027b76c362f10b646fce08dd34457533:29
-a4fc9c89f980d489cd69b5a9a5379279:28
-e7ca6d1abe4cd55a491cdf58eb8f4602:17
-6a6e8a76be9fd95bf68f4be6f6cf1bbf:4
-14a880d95891af72380815d1c91baf73:4
-34305e28da7351c9fe8a69e055e1c250:4
-f9a8c4364b7598dbeca0186f60e71ea6:4
-a2fa13ef7ba19d24adf59f8ecc12f0b4:4
-acd568e2592b223aaec17240031ed688:4
-cf994432aef1a88d7b82ffeecbbb96dc:4
-31927400c1d74c4dbcefe4c8c787ab8a:4
-94961af3e92bca38554a63bef2e453a6:4
-c562088538d99567be8dfdc54f830661:4
-459a7fef5f29ed6bafedadf927e315ee:4
-5c9d5847e56faea367acff998cb9e02b:4
-766fb7e580b7f7211bee8e77b98859b7:4
-53dc80d117986033f21a0b5f8609c87c:4
-3d8114f2ba5b5b51c547181a89a3466f:4
-89418ba487f62fbae3843e5dff5b9040:4
-ebe40ec90a872c67a1c27ae2c1eea950:4
-35462fa6c34cbb3610497e0e7ec5604a:4
-4f7b3ab75c9ad8d5b10fec60265a3809:4
-5123777f5c011dbd46ae38689b72a7d4:4
-38c98f315f4b01c1aa53a76a214a919e:4
-5cc99cf3cb2f9f5b2f3da2068a3c3b14:4
-9d57682c086fd2c37173c59f7df7f7f1:4
-25989076c985cb6136599803fbcb48d5:4
-a96139fddc921f9c720e39737bfc1e23:4
-bae7808504fe1ce016d86125d969287f:4
-b0767396a0f7f31b48ac96880c9cbf8b:4
-e2babe17ce0f4f5b30fee91c06c0e8fa:4
-d1c3eecefff445a312cfee0fa59d1431:4
-d5702e33cc1c8a04039657bec033a3e8:4
-1449b4ca79da7eaf4a3e04a2aa7b764d:4
-f50415b6582d0e25f6949d844bf93ee8:4
-3fef2395a8a091e7dea7a3b99dfeaea6:4
-03e302c7776c88610dafb333323263af:4
-b7d9936f2c7c87d2edac782f5403001b:4
-9cc9248b4494b77d7b40bbbf4dcef8b3:4
-043d915c3e3d58cb001a8b32deaffea3:4
-affb389d45f01bc18197bf0f49f4b118:4
-a4eb5a999085dfe2424b2896e38b25d1:4
-f9d3d4bc8a5944b63f51ae472e86813d:4
-a5e0393e11eadd75dd36fb5379ae018c:4
-5980bcaa38c00e4b837f2e9785bda3d2:4
-cc63328c0b60b9a790f8a1457dfaa511:4
-afdf18c7f6d8a437d84e390721b9b8b3:4
-b08cfcf9149aa4e94b5fb9695271c575:4
-10a9f2ae427efa8d37d59f435e83d62e:4
-d689449577a88d1b20d321d4e5fd32d9:4
-75298a5c8b84105fd9b9df7184dc110a:4
-5c8ed88fdd5d5ccb1ad7a61ecdf1ef9e:4
-35a4cc8262e2a04a80fc506d1551c2b7:4
-7ef43aabb95d2f3ad79438accc09cb4c:4
-aa6a754d71d091d666fea53f8769dfe9:4
-c8341c7fe17e7ae1484fdc25aef13df9:4
-4b6c6d5caf6e3d47ffd3dfdb329842f2:4
-a93c08a489709b0b6402127f3287a0a2:4
-500d600d630b1130b906355e694caf25:4
-e2f20c930af14f873866419d98bb33c5:4
-4f67d44d8a9c73b20f9dccb0fe708910:4
-3207a78e01966b63f857a845d312c5db:4
-dc772adf9d92e1330e1e6c1bc8bbdbea:4
-9a0f97a242e5b5c8bf70dd24775d973a:4
-c51604fe4030e75ce4ed3f545e470eee:4
-e8627b4481475f494ae99206e4d614a4:4
-e42535765b64f8f96c6a5b13c5aa30e9:4
-0389b0ce442166bc4298bd49f8ecfbff:4
-a3abcd010f778136d77b5c00575faa93:4
-d806274a83ce72b89939c7e1fcc7dbf1:4
-e6c5da69c2396e1a4c404a82509edb1e:4
-d62e86b3047119ec0941418ab6365bd4:4
-3c957e719e0ffce99b544fcc9a97ab3a:4
-638115e7b634086640ad6b300846ad6d:4
-f42bda5821df4a01dc97cb7d611a98a2:4
-15884ea4526d7af7c6dd5c5c89b81c97:4
-23fb4c5f959189ae9c90b968647934f3:4
-6b538e97c482ee0993607820484dcb08:4
-a5352e6f5efd89293cad68bde5359174:21
-4242f75bd579b5860e448896bd97161e:21
-0873c171286bf72adca073b8232909ad:21
-75fcc63a7299ec405cbbbae6c6f4124a:21
-f8e3f0c3200fb1dd44acfbd301d96bed:21
-3e972f384811f77fd62ce384910c7605:21
-746752a0385785d6c4a6af907b506ff8:21
-e40adee8247464101fcb01d083025c31:21
-f2c67cccefcb8826a54b39122e002062:21
-d96183e4ed9b9b2b075bd5f8c4364100:21
-0a8ab6673c7b4e87cb2faf2d7005362a:21
-810a68c672694a924abe0f0c59a9a394:21
-fe284dc96dc744beee1ddbb1a8e4c4ee:21
-711a52bc9e3eafe73e0c64c4ae6d5e6a:21
-d34d323d00595f4051cfe0621331317d:21
-542566d5040f1a84a95caf3c78e8d54f:21
-fadc0a0d51e6ccdc60992c058edd2711:21
-ccb12040fbbda66de180c11e6251049d:21
-052dc8ce89610356ae4c22dacb2668f6:21
-eb911a19cc8e2139a72a6bf025f0ef92:21
-8a9b11e0d4a3993f76485c069942489c:21
-b50ba38de9db0ecba8a7618f6138380b:21
-5a50c9965f9cd4ee5a321bbd530cc5b2:21
-301f4d37021f2c80999eedb2219554bd:21
-391832837fd0c5bd42b6d5d58dcd80f6:21
-535462c1df188932ba4375a241ac0a3b:21
-dd9d0f0182859fbb91bfe9cb3504becf:21
-517cb4d902b3c235f31b41bcd012b184:21
-4634339ec4a3dfef9d6a8a22e18c1354:21
-4015e477f56bfed839fc88a1ba749750:21
-be73d1320eb5c1229bc65e73f82039f3:21
-1c2ab72e50012527678e434999c9044a:21
-a09c3b287f292693c49b6ac6139f3aca:21
-67ce8616e53660f5898eaa146c7f3835:21
-eadaf0fe065d92924ebd1cade7c57e35:21
-b070068bc98b0eafc27ab9bb38e7cfbb:21
-5b728323f7668ae4ff0e895afaf15cea:21
-eea2af8f17a87b391353b3878325bbd1:21
-06065f8f4550b0f3530468ae56e4437b:21
-724567eb08f8a1a1dba97c82bd0a8d00:21
-aa462a0178a7561e7c756b157cbeeb38:21
-3af411c682f8264bb7c70794211c6248:21
-b972f761c2e0edfca60661ae2abf0218:21
-da13bac632b4f32422190d415f31bab8:21
-a7c7530bd46ed4e558e4d448f1ab9b7c:21
-e2c48e8e5e90a5dcc4cd070af2c1b20e:21
-0fc8da4c46369ca9dbcee59fb490681c:21
-5bb64f5d5c0764c9c1076e2c00ee3b3c:21
-f92f02539108f4c7034055e630ce5b1b:21
-21206168597de4cc003cf94bb2476441:21
-5adbafe8f34e077daeacc70305eed4f1:21
-80ffcde01a4a187455c15c7b11ae7c33:21
-4f89f7e86a6cb4ab329ea69918ae6f49:21
-d7d607c796c81475b0c57f2646b7e662:21
-0f88600ff268e2f32d56c63ce955aeba:21
-aeb824437b0a504c4a1a3109729a8817:21
-bd00bb3c1478ff33a781c1b48bfa9112:21
-b5653063b6061db848ef666076d4a44b:21
-eabb0f12ea319b61bdf53d9f4a6d7044:21
-1bf6b15eb573dea85120bcc84f419023:21
-71ae94c8959d23501b3a25269ed58f50:21
-c0673d423ec86b469092b2eedbf4e8db:21
-52986dd229c1677a8a8fea05023f7625:21
-364cf0eb88afb77fbc7209d2c1c7273e:21
-072b20ca146065654d29d72fa24fc89a:21
-50ddd3dc05b1198214cb57e9250d7076:21
-3eb1459c5b79cc1a562b145846a1b957:21
-d84c006146e1a54bba7a7122983ef085:21
-3a16ed65c968daf194834ce7a0225b8a:21
-6321e1881f171dc79e50d8e0cbc6ce7d:21
-5800fc71343c619b5e005bbab60912e0:21
-8e087b8bbcd92fe9389bf2f469f383f6:21
-e7e98eb2a51cc3cbf30bd532d8942294:21
-ba7c51aed579c0f1b473ef2035b15a17:21
-c173773ee22e94ab734c05bd504d3325:21
-5bef782b74608c4de25f4d053ae543a9:21
-169dfedc30ae5b95af848f3184206b4b:21
-549d9811413f0c4b84cd1e84290127f1:21
-668fa5f927950cbb34952cdb7e631eb8:21
-580fe13c6b95c05247c1770fff6d0fe9:21
-02d6383509108efe665de4a2aff24702:21
-68749591b1210c540ab306d416f49c68:21
-75269722fb78a1b61a850da9a73984fc:21
-58c819dd55c2558fed93faef88f45e89:21
-cd94f79ea296e6126eb563a6e82a73b3:21
-e479c8a303ccdaf45f673c78971d6a44:21
-ab652b1e99797dd3cc3bfb7e48db5d18:21
-4d3ab1362ff964f3947b437b16aba576:21
-53dea4d4defbb7512a31f4fa659437ae:21
-252e68c63ccea47b1d02a47837383c4e:21
-a9fff0b0f85b598b277aaf29fe537ad8:21
-caf732f52661f4c573b342a3d1da85bc:21
-9401285d5c41e4e0ee4581c62f4a2fc4:21
-5d0c7f0ad2c60ef78ae47f854b90a194:21
-983db0abe1d8976946a8a19eee412c9d:21
diff --git a/src/main/resources/api_database/api_database_api_level.ser b/src/main/resources/api_database/api_database_api_level.ser
deleted file mode 100644
index 984e786..0000000
--- a/src/main/resources/api_database/api_database_api_level.ser
+++ /dev/null
Binary files differ
diff --git a/src/main/resources/api_database/api_database_hash_lookup.ser b/src/main/resources/api_database/api_database_hash_lookup.ser
deleted file mode 100644
index d49a7f3..0000000
--- a/src/main/resources/api_database/api_database_hash_lookup.ser
+++ /dev/null
Binary files differ
diff --git a/src/test/java/com/android/tools/r8/SanityCheck.java b/src/test/java/com/android/tools/r8/SanityCheck.java
index c461475..9bce2a5 100644
--- a/src/test/java/com/android/tools/r8/SanityCheck.java
+++ b/src/test/java/com/android/tools/r8/SanityCheck.java
@@ -47,11 +47,7 @@
     }
     boolean licenseSeen = false;
     final Enumeration<? extends ZipEntry> entries = zipFile.entries();
-    Set<String> apiDatabaseFiles =
-        Sets.newHashSet(
-            "api_database/api_database_ambiguous.txt",
-            "api_database/api_database_api_level.ser",
-            "api_database/api_database_hash_lookup.ser");
+    Set<String> apiDatabaseFiles = Sets.newHashSet("api_database/new_api_database.ser");
     while (entries.hasMoreElements()) {
       ZipEntry entry = entries.nextElement();
       String name = entry.getName();
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java
index 6c3d36e..a409fd6 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGenerator.java
@@ -4,77 +4,81 @@
 
 package com.android.tools.r8.apimodel;
 
+import static com.android.tools.r8.androidapi.AndroidApiDataAccess.constantPoolHash;
+import static com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl.getNonExistingDescriptor;
+import static com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl.getUniqueDescriptorForReference;
+import static com.android.tools.r8.lightir.ByteUtils.isU2;
+import static com.android.tools.r8.lightir.ByteUtils.setBitAtIndex;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.androidapi.AndroidApiLevelHashingDatabaseImpl;
+import com.android.tools.r8.androidapi.AndroidApiDataAccess;
 import com.android.tools.r8.apimodel.AndroidApiVersionsXmlParser.ParsedApiClass;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMember;
+import com.android.tools.r8.graph.DexLibraryClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexReference;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.LibraryClass;
 import com.android.tools.r8.references.ClassReference;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.Pair;
-import com.android.tools.r8.utils.structural.DefaultHashingVisitor;
-import com.android.tools.r8.utils.structural.HasherWrapper;
-import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.ThrowingBiConsumer;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
 import java.io.FileOutputStream;
-import java.io.ObjectOutputStream;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
+import java.io.IOException;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.function.BiConsumer;
 
 public class AndroidApiHashingDatabaseBuilderGenerator extends TestBase {
 
   /**
    * Generate the information needed for looking up api level of references in the android.jar. This
-   * method will generate three different files and store them in the passed paths. pathToIndices
-   * will be an int array with hashcode entries for each DexReference. The pathToApiLevels is a byte
-   * array with a byte describing the api level that the index in pathToIndices has. To ensure that
-   * this lookup work the generate algorithm tracks all colliding hash codes such and stores them in
-   * another format. The indices map is populated with all colliding entries and a -1 is inserted
-   * for the api level.
+   * method will generate one single database file where the format is as follows (uX is X number of
+   * unsigned bytes):
+   *
+   * <pre>
+   * constant_pool_size: u4
+   * constant_pool:      [constant_pool_size * payload_entry]
+   * constant_pool_map:  [0..max_hash(DexString) * payload_entry]
+   * api_map:            [0..max_hash(DexReference) * payload_entry]
+   * payload             raw data.
+   *
+   * payload_entry: u4:relative_offset_from_payload_start + u2:length
+   * </pre>
+   *
+   * For hash_definitions and entries see {@code AndroidApiDataAccess}.
    */
   public static void generate(
-      List<ParsedApiClass> apiClasses,
-      Path pathToIndices,
-      Path pathToApiLevels,
-      Path ambiguousDefinitions,
-      AndroidApiLevel androidJarApiLevel)
+      List<ParsedApiClass> apiClasses, Path pathToApiLevels, AndroidApiLevel androidJarApiLevel)
       throws Exception {
     Map<ClassReference, Map<DexMethod, AndroidApiLevel>> methodMap = new HashMap<>();
     Map<ClassReference, Map<DexField, AndroidApiLevel>> fieldMap = new HashMap<>();
     Map<ClassReference, ParsedApiClass> lookupMap = new HashMap<>();
-    Path androidJar = ToolHelper.getAndroidJar(androidJarApiLevel);
-    AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(AndroidApp.builder().addLibraryFile(androidJar).build());
-    DexItemFactory factory = appView.dexItemFactory();
 
-    Map<Integer, AndroidApiLevel> apiLevelMap = new HashMap<>();
-    Map<Integer, Pair<DexReference, AndroidApiLevel>> reverseMap = new HashMap<>();
-    Map<AndroidApiLevel, Set<DexReference>> ambiguousMap = new HashMap<>();
-    // Populate maps for faster lookup.
-    for (ParsedApiClass apiClass : apiClasses) {
-      DexType type = factory.createType(apiClass.getClassReference().getDescriptor());
-      AndroidApiLevel existing = apiLevelMap.put(type.hashCode(), apiClass.getApiLevel());
-      assert existing == null;
-      reverseMap.put(type.hashCode(), Pair.create(type, apiClass.getApiLevel()));
-    }
+    Map<DexReference, AndroidApiLevel> referenceMap = new HashMap<>();
+
+    Path androidJar = ToolHelper.getAndroidJar(androidJarApiLevel);
+    AppView<AppInfoWithClassHierarchy> appView =
+        computeAppViewWithClassHierarchy(AndroidApp.builder().addLibraryFile(androidJar).build());
+    DexItemFactory factory = appView.dexItemFactory();
 
     for (ParsedApiClass apiClass : apiClasses) {
       Map<DexMethod, AndroidApiLevel> methodsForApiClass = new HashMap<>();
@@ -91,27 +95,44 @@
       methodMap.put(apiClass.getClassReference(), methodsForApiClass);
       fieldMap.put(apiClass.getClassReference(), fieldsForApiClass);
       lookupMap.put(apiClass.getClassReference(), apiClass);
-    }
 
-    BiConsumer<DexReference, AndroidApiLevel> addConsumer =
-        addReferenceToMaps(apiLevelMap, reverseMap, ambiguousMap);
+      referenceMap.put(
+          factory.createType(apiClass.getClassReference().getDescriptor()), apiClass.getApiLevel());
+    }
 
     for (ParsedApiClass apiClass : apiClasses) {
       computeAllReferencesInHierarchy(
-              lookupMap,
-              factory,
-              factory.createType(apiClass.getClassReference().getDescriptor()),
-              apiClass,
-              AndroidApiLevel.B,
-              new IdentityHashMap<>())
-          .forEach(addConsumer);
+          lookupMap,
+          factory,
+          factory.createType(apiClass.getClassReference().getDescriptor()),
+          apiClass,
+          AndroidApiLevel.B,
+          referenceMap);
     }
 
+    assert ensureAllPublicMethodsAreMapped(
+        appView, lookupMap, methodMap, fieldMap, referenceMap, androidJar);
+
+    try (FileOutputStream fileOutputStream = new FileOutputStream(pathToApiLevels.toFile())) {
+      DataOutputStream dataOutputStream = new DataOutputStream(fileOutputStream);
+      generateDatabase(referenceMap, dataOutputStream);
+    }
+  }
+
+  private static boolean ensureAllPublicMethodsAreMapped(
+      AppView<AppInfoWithClassHierarchy> appView,
+      Map<ClassReference, ParsedApiClass> lookupMap,
+      Map<ClassReference, Map<DexMethod, AndroidApiLevel>> methodMap,
+      Map<ClassReference, Map<DexField, AndroidApiLevel>> fieldMap,
+      Map<DexReference, AndroidApiLevel> referenceMap,
+      Path androidJar) {
     Map<DexType, String> missingMemberInformation = new IdentityHashMap<>();
-    for (LibraryClass clazz : appView.app().asDirect().libraryClasses()) {
+    for (DexLibraryClass clazz : appView.app().asDirect().libraryClasses()) {
       ParsedApiClass parsedApiClass = lookupMap.get(clazz.getClassReference());
       if (parsedApiClass == null) {
-        missingMemberInformation.put(clazz.getType(), "Could not be found in " + androidJar);
+        if (clazz.isPublic()) {
+          missingMemberInformation.put(clazz.getType(), "Could not be found in " + androidJar);
+        }
         continue;
       }
       StringBuilder classBuilder = new StringBuilder();
@@ -120,8 +141,8 @@
       clazz.forEachClassField(
           field -> {
             if (field.getAccessFlags().isPublic()
-                && getApiLevelFromReference(field.getReference(), apiLevelMap, ambiguousMap) == null
-                && field.toSourceString().contains("this$0")) {
+                && referenceMap.get(field.getReference()) == null
+                && !field.toSourceString().contains("this$0")) {
               classBuilder.append("  ").append(field).append(" is missing\n");
             }
           });
@@ -130,9 +151,8 @@
       clazz.forEachClassMethod(
           method -> {
             if (method.getAccessFlags().isPublic()
-                && getApiLevelFromReference(method.getReference(), apiLevelMap, ambiguousMap)
-                    == null
-                && !factory.objectMembers.isObjectMember(method.getReference())) {
+                && referenceMap.get(method.getReference()) == null
+                && !appView.dexItemFactory().objectMembers.isObjectMember(method.getReference())) {
               classBuilder.append("  ").append(method).append(" is missing\n");
             }
           });
@@ -143,141 +163,205 @@
 
     // api-versions.xml do not encode all members of StringBuffers and StringBuilders, check that we
     // only have missing definitions for those two classes.
-    assert missingMemberInformation.size() == 2;
-    assert missingMemberInformation.containsKey(factory.stringBufferType);
-    assert missingMemberInformation.containsKey(factory.stringBuilderType);
-
-    int[] indices = new int[apiLevelMap.size()];
-    byte[] apiLevel = new byte[apiLevelMap.size()];
-    ArrayList<Integer> integers = new ArrayList<>(apiLevelMap.keySet());
-    for (int i = 0; i < integers.size(); i++) {
-      indices[i] = integers.get(i);
-      AndroidApiLevel androidApiLevel = apiLevelMap.get(integers.get(i));
-      assert androidApiLevel != null;
-      apiLevel[i] =
-          (byte)
-              (androidApiLevel == AndroidApiLevel.ANDROID_PLATFORM
-                  ? -1
-                  : androidApiLevel.getLevel());
-    }
-
-    try (FileOutputStream fileOutputStream = new FileOutputStream(pathToIndices.toFile());
-        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
-      objectOutputStream.writeObject(indices);
-    }
-
-    try (FileOutputStream fileOutputStream = new FileOutputStream(pathToApiLevels.toFile());
-        ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream)) {
-      objectOutputStream.writeObject(apiLevel);
-    }
-
-    String ambiguousMapSerialized = serializeAmbiguousMap(ambiguousMap);
-    Files.write(ambiguousDefinitions, ambiguousMapSerialized.getBytes(StandardCharsets.UTF_8));
+    assert missingMemberInformation.size() == 7;
+    assert missingMemberInformation.containsKey(appView.dexItemFactory().stringBufferType);
+    assert missingMemberInformation.containsKey(appView.dexItemFactory().stringBuilderType);
+    // TODO(b/231126636): api-versions.xml has missing definitions for the below classes.
+    assert missingMemberInformation.containsKey(
+        appView.dexItemFactory().createType("Ljava/util/concurrent/ConcurrentHashMap$KeySetView;"));
+    assert missingMemberInformation.containsKey(
+        appView.dexItemFactory().createType("Ljava/time/chrono/ThaiBuddhistDate;"));
+    assert missingMemberInformation.containsKey(
+        appView.dexItemFactory().createType("Ljava/time/chrono/HijrahDate;"));
+    assert missingMemberInformation.containsKey(
+        appView.dexItemFactory().createType("Ljava/time/chrono/JapaneseDate;"));
+    assert missingMemberInformation.containsKey(
+        appView.dexItemFactory().createType("Ljava/time/chrono/MinguoDate;"));
+    return true;
   }
 
-  /** This will serialize a collection of DexReferences with the api level they correspond to. */
-  private static String serializeAmbiguousMap(
-      Map<AndroidApiLevel, Set<DexReference>> ambiguousMap) {
-    Set<String> seen = new HashSet<>();
-    StringBuilder sb = new StringBuilder();
-    ambiguousMap.forEach(
-        (apiLevel, references) -> {
-          references.forEach(
-              reference -> {
-                HasherWrapper defaultHasher = AndroidApiLevelHashingDatabaseImpl.getDefaultHasher();
-                reference.accept(
-                    type -> DefaultHashingVisitor.run(type, defaultHasher, DexType::acceptHashing),
-                    field ->
-                        DefaultHashingVisitor.run(
-                            field, defaultHasher, StructuralItem::acceptHashing),
-                    method ->
-                        DefaultHashingVisitor.run(
-                            method, defaultHasher, StructuralItem::acceptHashing));
-                String referenceHash = defaultHasher.hash().toString();
-                if (!seen.add(referenceHash)) {
-                  throw new RuntimeException(
-                      "More than one item with key: <"
-                          + referenceHash
-                          + ">. The choice of key encoding will need to change to generate a valid"
-                          + " api database");
-                }
-                sb.append(referenceHash).append(":").append(apiLevel.getLevel()).append("\n");
-              });
+  private static class ConstantPool {
+
+    private final IntBox intBox = new IntBox(0);
+    private final Map<DexString, Integer> pool = new LinkedHashMap<>();
+
+    public int getOrAdd(DexString string) {
+      return pool.computeIfAbsent(string, ignored -> intBox.getAndIncrement());
+    }
+
+    public void forEach(ThrowingBiConsumer<DexString, Integer, IOException> consumer)
+        throws IOException {
+      for (Entry<DexString, Integer> entry : pool.entrySet()) {
+        consumer.accept(entry.getKey(), entry.getValue());
+      }
+    }
+
+    public int size() {
+      return pool.size();
+    }
+  }
+
+  private static int setUniqueConstantPoolEntry(int id) {
+    return setBitAtIndex(id, 32);
+  }
+
+  public static void generateDatabase(
+      Map<DexReference, AndroidApiLevel> referenceMap, DataOutputStream outputStream)
+      throws Exception {
+    Map<Integer, List<Pair<DexReference, AndroidApiLevel>>> generationMap = new HashMap<>();
+    ConstantPool constantPool = new ConstantPool();
+
+    int constantPoolMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForConstantPoolMap();
+    int apiMapSize = 1 << AndroidApiDataAccess.entrySizeInBitsForApiLevelMap();
+
+    for (Entry<DexReference, AndroidApiLevel> entry : referenceMap.entrySet()) {
+      int newCode = AndroidApiDataAccess.apiLevelHash(entry.getKey());
+      assert newCode >= 0 && newCode <= apiMapSize;
+      generationMap
+          .computeIfAbsent(newCode, ignoreKey(ArrayList::new))
+          .add(Pair.create(entry.getKey(), entry.getValue()));
+    }
+
+    Set<String> uniqueHashes = new HashSet<>();
+    Map<Integer, Pair<Integer, Integer>> offsetMap = new HashMap<>();
+    ByteArrayOutputStream payload = new ByteArrayOutputStream();
+
+    // Serialize api map into payload. This will also generate the entire needed constant pool.
+    for (Entry<Integer, List<Pair<DexReference, AndroidApiLevel>>> entry :
+        generationMap.entrySet()) {
+      int startingOffset = payload.size();
+      int length = serializeIntoPayload(entry.getValue(), payload, constantPool, uniqueHashes);
+      offsetMap.put(entry.getKey(), Pair.create(startingOffset, length));
+    }
+
+    // Write constant pool size <u4:size>.
+    outputStream.writeInt(constantPool.size());
+
+    // Write constant pool consisting of <u4:payload_offset><u2:length>.
+    IntBox lastReadIndex = new IntBox(-1);
+    constantPool.forEach(
+        (string, id) -> {
+          assert id > lastReadIndex.getAndIncrement();
+          outputStream.writeInt(payload.size());
+          outputStream.writeShort(string.content.length);
+          payload.write(string.content);
         });
-    return sb.toString();
-  }
 
-  private static AndroidApiLevel getApiLevelFromReference(
-      DexReference reference,
-      Map<Integer, AndroidApiLevel> apiLevelMap,
-      Map<AndroidApiLevel, Set<DexReference>> ambiguousMap) {
-    int hashCode = reference.hashCode();
-    AndroidApiLevel androidApiLevel = apiLevelMap.get(hashCode);
-    if (androidApiLevel == null) {
-      return null;
-    }
-    if (androidApiLevel == AndroidApiLevel.ANDROID_PLATFORM) {
-      for (Entry<AndroidApiLevel, Set<DexReference>> apiAmbiguousSet : ambiguousMap.entrySet()) {
-        if (apiAmbiguousSet.getValue().contains(reference)) {
-          return apiAmbiguousSet.getKey();
-        }
-      }
-      return null;
-    } else {
-      return androidApiLevel;
-    }
-  }
+    // Serialize hash lookup table for constant pool.
+    Map<Integer, List<Integer>> constantPoolLookupTable = new HashMap<>();
+    constantPool.forEach(
+        (string, id) -> {
+          int constantPoolHash = constantPoolHash(string);
+          assert constantPoolHash >= 0 && constantPoolHash <= constantPoolMapSize;
+          constantPoolLookupTable
+              .computeIfAbsent(constantPoolHash, ignoreKey(ArrayList::new))
+              .add(id);
+        });
 
-  private static BiConsumer<DexReference, AndroidApiLevel> addReferenceToMaps(
-      Map<Integer, AndroidApiLevel> apiLevelMap,
-      Map<Integer, Pair<DexReference, AndroidApiLevel>> reverseMap,
-      Map<AndroidApiLevel, Set<DexReference>> ambiguousMap) {
-    return ((reference, apiLevel) -> {
-      AndroidApiLevel existingMethod = apiLevelMap.put(reference.hashCode(), apiLevel);
-      if (existingMethod == AndroidApiLevel.ANDROID_PLATFORM) {
-        addAmbiguousEntry(apiLevel, reference, ambiguousMap);
-      } else if (existingMethod != null) {
-        apiLevelMap.put(reference.hashCode(), AndroidApiLevel.ANDROID_PLATFORM);
-        Pair<DexReference, AndroidApiLevel> existingPair = reverseMap.get(reference.hashCode());
-        addAmbiguousEntry(existingPair.getSecond(), existingPair.getFirst(), ambiguousMap);
-        addAmbiguousEntry(apiLevel, reference, ambiguousMap);
+    int[] constantPoolEntries = new int[constantPoolMapSize];
+    int[] constantPoolEntryLengths = new int[constantPoolMapSize];
+    for (Entry<Integer, List<Integer>> entry : constantPoolLookupTable.entrySet()) {
+      // Tag if we have a unique value
+      if (entry.getValue().size() == 1) {
+        int id = entry.getValue().get(0);
+        constantPoolEntries[entry.getKey()] = setUniqueConstantPoolEntry(id);
       } else {
-        reverseMap.put(reference.hashCode(), Pair.create(reference, apiLevel));
+        constantPoolEntries[entry.getKey()] = payload.size();
+        ByteArrayOutputStream temp = new ByteArrayOutputStream();
+        for (Integer id : entry.getValue()) {
+          temp.write(intToShortEncodedByteArray(id));
+        }
+        payload.write(temp.toByteArray());
+        constantPoolEntryLengths[entry.getKey()] = temp.size();
       }
-    });
+    }
+    // Write constant pool lookup entries consisting of <u4:payload_offset><u2:length>
+    for (int i = 0; i < constantPoolEntries.length; i++) {
+      outputStream.writeInt(constantPoolEntries[i]);
+      outputStream.writeShort(constantPoolEntryLengths[i]);
+    }
+
+    int[] apiOffsets = new int[apiMapSize];
+    int[] apiOffsetLengths = new int[apiMapSize];
+    for (Entry<Integer, Pair<Integer, Integer>> hashIndexAndOffset : offsetMap.entrySet()) {
+      assert apiOffsets[hashIndexAndOffset.getKey()] == 0;
+      Pair<Integer, Integer> value = hashIndexAndOffset.getValue();
+      int offset = value.getFirst();
+      int length = value.getSecond();
+      apiOffsets[hashIndexAndOffset.getKey()] = offset;
+      apiOffsetLengths[hashIndexAndOffset.getKey()] = length;
+    }
+
+    // Write api lookup entries consisting of <u4:payload_offset><u2:length>
+    for (int i = 0; i < apiOffsets.length; i++) {
+      outputStream.writeInt(apiOffsets[i]);
+      outputStream.writeShort(apiOffsetLengths[i]);
+    }
+
+    // Write the payload.
+    outputStream.write(payload.toByteArray());
   }
 
-  private static void addAmbiguousEntry(
-      AndroidApiLevel apiLevel,
-      DexReference reference,
-      Map<AndroidApiLevel, Set<DexReference>> ambiguousMap) {
-    ambiguousMap.computeIfAbsent(apiLevel, ignored -> new HashSet<>()).add(reference);
+  /** This will serialize a collection of DexReferences and apis into a byte stream. */
+  private static int serializeIntoPayload(
+      List<Pair<DexReference, AndroidApiLevel>> pairs,
+      ByteArrayOutputStream payload,
+      ConstantPool constantPool,
+      Set<String> seen)
+      throws IOException {
+    ByteArrayOutputStream temp = new ByteArrayOutputStream();
+    for (Pair<DexReference, AndroidApiLevel> pair : pairs) {
+      byte[] uniqueDescriptorForReference =
+          getUniqueDescriptorForReference(pair.getFirst(), constantPool::getOrAdd);
+      assert uniqueDescriptorForReference != getNonExistingDescriptor();
+      if (!seen.add(Arrays.toString(uniqueDescriptorForReference))) {
+        throw new Unreachable("Hash is not unique");
+      }
+      temp.write(intToShortEncodedByteArray(uniqueDescriptorForReference.length));
+      temp.write(uniqueDescriptorForReference);
+      temp.write((byte) pair.getSecond().getLevel());
+    }
+    byte[] tempArray = temp.toByteArray();
+    payload.write(tempArray);
+    return tempArray.length;
   }
 
-  @SuppressWarnings("unchecked")
-  private static <T extends DexMember<?, ?>>
-      Map<T, AndroidApiLevel> computeAllReferencesInHierarchy(
-          Map<ClassReference, ParsedApiClass> lookupMap,
-          DexItemFactory factory,
-          DexType holder,
-          ParsedApiClass apiClass,
-          AndroidApiLevel linkLevel,
-          Map<T, AndroidApiLevel> additionMap) {
+  public static byte[] intToShortEncodedByteArray(int value) {
+    assert isU2(value);
+    byte[] bytes = new byte[2];
+    bytes[0] = (byte) (value >> 8);
+    bytes[1] = (byte) value;
+    return bytes;
+  }
+
+  private static void computeAllReferencesInHierarchy(
+      Map<ClassReference, ParsedApiClass> lookupMap,
+      DexItemFactory factory,
+      DexType holder,
+      ParsedApiClass apiClass,
+      AndroidApiLevel linkLevel,
+      Map<DexReference, AndroidApiLevel> additionMap) {
     if (!apiClass.getClassReference().getDescriptor().equals(factory.objectDescriptor.toString())) {
       apiClass.visitMethodReferences(
           (apiLevel, methodReferences) -> {
             methodReferences.forEach(
                 methodReference -> {
-                  T member = (T) factory.createMethod(methodReference).withHolder(holder, factory);
-                  addIfNewOrApiLevelIsLower(linkLevel, additionMap, apiLevel, member);
+                  addIfNewOrApiLevelIsLower(
+                      linkLevel,
+                      additionMap,
+                      apiLevel,
+                      factory.createMethod(methodReference).withHolder(holder, factory));
                 });
           });
       apiClass.visitFieldReferences(
           (apiLevel, fieldReferences) -> {
             fieldReferences.forEach(
                 fieldReference -> {
-                  T member = (T) factory.createField(fieldReference).withHolder(holder, factory);
-                  addIfNewOrApiLevelIsLower(linkLevel, additionMap, apiLevel, member);
+                  addIfNewOrApiLevelIsLower(
+                      linkLevel,
+                      additionMap,
+                      apiLevel,
+                      factory.createField(fieldReference).withHolder(holder, factory));
                 });
           });
       apiClass.visitSuperType(
@@ -301,14 +385,13 @@
                 additionMap);
           });
     }
-    return additionMap;
   }
 
-  private static <T extends DexMember<?, ?>> void addIfNewOrApiLevelIsLower(
+  private static void addIfNewOrApiLevelIsLower(
       AndroidApiLevel linkLevel,
-      Map<T, AndroidApiLevel> additionMap,
+      Map<DexReference, AndroidApiLevel> additionMap,
       AndroidApiLevel apiLevel,
-      T member) {
+      DexReference member) {
     AndroidApiLevel currentApiLevel = apiLevel.max(linkLevel);
     AndroidApiLevel existingApiLevel = additionMap.get(member);
     if (existingApiLevel == null || currentApiLevel.isLessThanOrEqualTo(existingApiLevel)) {
diff --git a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
index 599829b..dcf406d 100644
--- a/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
+++ b/src/test/java/com/android/tools/r8/apimodel/AndroidApiHashingDatabaseBuilderGeneratorTest.java
@@ -6,7 +6,6 @@
 
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -21,14 +20,10 @@
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.IntBox;
 import com.google.common.collect.ImmutableList;
-import java.io.FileInputStream;
-import java.io.ObjectInputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
@@ -39,12 +34,13 @@
 public class AndroidApiHashingDatabaseBuilderGeneratorTest extends TestBase {
 
   protected final TestParameters parameters;
-  private static final Path API_DATABASE_HASH_LOOKUP =
-      Paths.get(ToolHelper.RESOURCES_DIR, "api_database", "api_database_hash_lookup.ser");
-  private static final Path API_DATABASE_API_LEVEL =
-      Paths.get(ToolHelper.RESOURCES_DIR, "api_database", "api_database_api_level.ser");
-  private static final Path API_DATABASE_AMBIGUOUS =
-      Paths.get(ToolHelper.RESOURCES_DIR, "api_database", "api_database_ambiguous.txt");
+  private static final Path API_DATABASE_FOLDER =
+      Paths.get(ToolHelper.THIRD_PARTY_DIR, "api_database");
+  private static final Path API_DATABASE =
+      API_DATABASE_FOLDER
+          .resolve("api_database")
+          .resolve("resources")
+          .resolve("new_api_database.ser");
 
   // Update the API_LEVEL below to have the database generated for a new api level.
   private static final AndroidApiLevel API_LEVEL = AndroidApiLevel.S;
@@ -60,14 +56,10 @@
 
   private static class GenerateDatabaseResourceFilesResult {
 
-    private final Path indices;
     private final Path apiLevels;
-    private final Path ambiguous;
 
-    public GenerateDatabaseResourceFilesResult(Path indices, Path apiLevels, Path ambiguous) {
-      this.indices = indices;
+    public GenerateDatabaseResourceFilesResult(Path apiLevels) {
       this.apiLevels = apiLevels;
-      this.ambiguous = ambiguous;
     }
   }
 
@@ -82,12 +74,9 @@
       List<ParsedApiClass> apiClasses, AndroidApiLevel androidJarApiLevel) throws Exception {
     TemporaryFolder temp = new TemporaryFolder();
     temp.create();
-    Path indices = temp.newFile("indices.ser").toPath();
-    Path apiLevels = temp.newFile("apiLevels.ser").toPath();
-    Path ambiguous = temp.newFile("ambiguous.ser").toPath();
-    AndroidApiHashingDatabaseBuilderGenerator.generate(
-        apiClasses, indices, apiLevels, ambiguous, androidJarApiLevel);
-    return new GenerateDatabaseResourceFilesResult(indices, apiLevels, ambiguous);
+    Path apiLevels = temp.newFile("new_api_levels.ser").toPath();
+    AndroidApiHashingDatabaseBuilderGenerator.generate(apiClasses, apiLevels, androidJarApiLevel);
+    return new GenerateDatabaseResourceFilesResult(apiLevels);
   }
 
   @Test
@@ -119,9 +108,7 @@
   @Test
   public void testDatabaseGenerationUpToDate() throws Exception {
     GenerateDatabaseResourceFilesResult result = generateResourcesFiles();
-    TestBase.filesAreEqual(result.indices, API_DATABASE_HASH_LOOKUP);
-    TestBase.filesAreEqual(result.apiLevels, API_DATABASE_API_LEVEL);
-    TestBase.filesAreEqual(result.ambiguous, API_DATABASE_AMBIGUOUS);
+    TestBase.filesAreEqual(result.apiLevels, API_DATABASE);
   }
 
   @Test
@@ -169,27 +156,21 @@
    *
    * <p>The generated jar depends on r8NoManifestWithoutDeps.
    *
-   * <p>If the generated jar passes tests it will be moved to third_party/android_jar/api-database/
-   * and override the current file in there.
+   * <p>If the generated jar passes tests it will be moved and overwrite
+   * third_party/api_database/new_api_database.ser.
    */
   public static void main(String[] args) throws Exception {
     GenerateDatabaseResourceFilesResult result = generateResourcesFiles();
-    verifyNoDuplicateHashes(result.indices);
-    Files.move(result.indices, API_DATABASE_HASH_LOOKUP, REPLACE_EXISTING);
-    Files.move(result.apiLevels, API_DATABASE_API_LEVEL, REPLACE_EXISTING);
-    Files.move(result.ambiguous, API_DATABASE_AMBIGUOUS, REPLACE_EXISTING);
-  }
-
-  private static void verifyNoDuplicateHashes(Path indicesPath) throws Exception {
-    Set<Integer> elements = new HashSet<>();
-    int[] indices;
-    try (FileInputStream fileInputStream = new FileInputStream(indicesPath.toFile());
-        ObjectInputStream indicesObjectStream = new ObjectInputStream(fileInputStream)) {
-      indices = (int[]) indicesObjectStream.readObject();
-      for (int index : indices) {
-        assertTrue(elements.add(index));
-      }
-    }
-    assertEquals(elements.size(), indices.length);
+    API_DATABASE.toFile().mkdirs();
+    Files.move(result.apiLevels, API_DATABASE, REPLACE_EXISTING);
+    System.out.println(
+        "Updated file in: "
+            + API_DATABASE
+            + "\nRemember to upload to cloud storage:"
+            + "\n(cd "
+            + API_DATABASE_FOLDER
+            + " && upload_to_google_storage.py -a --bucket r8-deps "
+            + API_DATABASE_FOLDER.getFileName()
+            + ")");
   }
 }
diff --git a/third_party/api_database/api_database.tar.gz.sha1 b/third_party/api_database/api_database.tar.gz.sha1
new file mode 100644
index 0000000..145b236
--- /dev/null
+++ b/third_party/api_database/api_database.tar.gz.sha1
@@ -0,0 +1 @@
+c6833a3d1e8dde96f02cbbd35c581b9d49575d79
\ No newline at end of file