Optimize DexLimitTracker with strong string interning for faster Multidex sharding. PiperOrigin-RevId: 974808907 Change-Id: Iedb642b5afd9ecab1abd34b8e641553d89a441b6
diff --git a/src/tools/java/com/google/devtools/build/android/dexer/DexLimitTracker.java b/src/tools/java/com/google/devtools/build/android/dexer/DexLimitTracker.java index a836b07..6ff0251 100644 --- a/src/tools/java/com/google/devtools/build/android/dexer/DexLimitTracker.java +++ b/src/tools/java/com/google/devtools/build/android/dexer/DexLimitTracker.java
@@ -20,6 +20,7 @@ import com.android.dex.TypeList; import com.google.common.collect.Interner; import com.google.common.collect.Interners; +import com.google.common.collect.Sets; import java.util.Collections; import java.util.HashSet; @@ -30,15 +31,34 @@ class DexLimitTracker { private static final ThreadLocal<Interner<String>> threadLocalInterner = - ThreadLocal.withInitial(Interners::newWeakInterner); + ThreadLocal.withInitial(Interners::newStrongInterner); - private final HashSet<String> fieldsSeen = new HashSet<>(); - private final HashSet<String> methodsSeen = new HashSet<>(); - private final HashSet<String> typesSeen = new HashSet<>(); + /** + * Upper bound for initial HashSet capacity. Avoids repeated resizing and rehashing cycles (from + * the default capacity of 16) for typical multidex shards without prematurely allocating full + * 64K-entry backing tables for smaller archives. + */ + private static final int MAX_INITIAL_EXPECTED_SIZE = 8192; + + /** + * Removes the thread-local interner instance for the current thread to prevent accumulation + * across multiple builds in persistent worker processes. + */ + public static void clearInterner() { + threadLocalInterner.remove(); + } + + private final HashSet<String> fieldsSeen; + private final HashSet<String> methodsSeen; + private final HashSet<String> typesSeen; private final int maxNumberOfIdxPerDex; public DexLimitTracker(int maxNumberOfIdxPerDex) { this.maxNumberOfIdxPerDex = maxNumberOfIdxPerDex; + int initialCapacity = Math.min(maxNumberOfIdxPerDex, MAX_INITIAL_EXPECTED_SIZE); + this.fieldsSeen = Sets.newHashSetWithExpectedSize(initialCapacity); + this.methodsSeen = Sets.newHashSetWithExpectedSize(initialCapacity); + this.typesSeen = Sets.newHashSetWithExpectedSize(initialCapacity); } /** @@ -57,6 +77,7 @@ fieldsSeen.clear(); methodsSeen.clear(); typesSeen.clear(); + threadLocalInterner.remove(); } public void track(Dex dexFile) { @@ -81,44 +102,55 @@ } static DexTrackerInfo create(Dex dexFile) { + int stringCount = dexFile.getTableOfContents().stringIds.size; + String[] stringCache = new String[stringCount]; + int typeCount = dexFile.typeIds().size(); String[] types = new String[typeCount]; for (int typeIndex = 0; typeIndex < typeCount; ++typeIndex) { - types[typeIndex] = typeName(dexFile, typeIndex); + int stringIndex = dexFile.typeIds().get(typeIndex); + types[typeIndex] = getString(dexFile, stringIndex, stringCache); } int fieldCount = dexFile.fieldIds().size(); String[] fields = new String[fieldCount]; for (int fieldIndex = 0; fieldIndex < fieldCount; ++fieldIndex) { - fields[fieldIndex] = fieldSignature(dexFile, fieldIndex, types); + fields[fieldIndex] = fieldSignature(dexFile, fieldIndex, types, stringCache); } int methodCount = dexFile.methodIds().size(); String[] methods = new String[methodCount]; for (int methodIndex = 0; methodIndex < methodCount; ++methodIndex) { - methods[methodIndex] = methodSignature(dexFile, methodIndex, types); + methods[methodIndex] = methodSignature(dexFile, methodIndex, types, stringCache); } return new DexTrackerInfo(fields, methods, types); } } - private static String typeName(Dex dex, int typeIndex) { - return threadLocalInterner.get().intern(dex.typeNames().get(typeIndex)); + private static String getString(Dex dex, int stringIndex, String[] stringCache) { + String s = stringCache[stringIndex]; + if (s == null) { + s = threadLocalInterner.get().intern(dex.strings().get(stringIndex)); + stringCache[stringIndex] = s; + } + return s; } - private static String fieldSignature(Dex dex, int fieldIndex, String[] typeCache) { + private static String fieldSignature( + Dex dex, int fieldIndex, String[] typeCache, String[] stringCache) { FieldId field = dex.fieldIds().get(fieldIndex); - String name = dex.strings().get(field.getNameIndex()); + String name = getString(dex, field.getNameIndex(), stringCache); String declaringClass = typeCache[field.getDeclaringClassIndex()]; String type = typeCache[field.getTypeIndex()]; return threadLocalInterner.get().intern(declaringClass + "." + name + ":" + type); } - private static String methodSignature(Dex dex, int methodIndex, String[] typeCache) { + private static String methodSignature( + Dex dex, int methodIndex, String[] typeCache, String[] stringCache) { MethodId method = dex.methodIds().get(methodIndex); ProtoId proto = dex.protoIds().get(method.getProtoIndex()); - String name = dex.strings().get(method.getNameIndex()); + String name = getString(dex, method.getNameIndex(), stringCache); String declaringClass = typeCache[method.getDeclaringClassIndex()]; String returnType = typeCache[proto.getReturnTypeIndex()]; TypeList parameterTypeIndices = dex.readTypeList(proto.getParametersOffset()); @@ -128,6 +160,6 @@ } return threadLocalInterner .get() - .intern(declaringClass + "." + name + ":" + returnType + "(" + parameterTypes + ")"); + .intern(declaringClass + "." + name + "(" + parameterTypes + ")" + returnType); } }
diff --git a/src/tools/javatests/com/google/devtools/build/android/dexer/DexLimitTrackerTest.java b/src/tools/javatests/com/google/devtools/build/android/dexer/DexLimitTrackerTest.java index 1788198..744c43f 100644 --- a/src/tools/javatests/com/google/devtools/build/android/dexer/DexLimitTrackerTest.java +++ b/src/tools/javatests/com/google/devtools/build/android/dexer/DexLimitTrackerTest.java
@@ -122,10 +122,10 @@ assertThat(info.methods) .asList() .containsExactly( - "Lbase/SimpleClass;.<init>:V()", - "Lbase/SimpleClass;.method:V()", - "Lbase/SimpleClass;.methodWithParams:V(ILjava/lang/String;)", - "Ljava/lang/Object;.<init>:V()"); + "Lbase/SimpleClass;.<init>()V", + "Lbase/SimpleClass;.method()V", + "Lbase/SimpleClass;.methodWithParams(ILjava/lang/String;)V", + "Ljava/lang/Object;.<init>()V"); // Verify types assertThat(info.types).asList().contains("Lbase/SimpleClass;"); @@ -206,4 +206,50 @@ assertTrue("Field " + field.getName() + " must be final", Modifier.isFinal(modifiers)); } } + + @Test + public void testTrackMethodsAndTypesLimits() throws Exception { + Dex dexSimple = loadDexFromArchive(simpleDexArchive); // 4 methods, 1 field + + // Set method limit to 3 (less than 4) -> should be outside limits + DexLimitTracker methodTracker = new DexLimitTracker(3); + methodTracker.track(dexSimple); + assertThat(methodTracker.outsideLimits()).isTrue(); + + // Set type limit to 1 -> should be outside limits + DexLimitTracker typeTracker = new DexLimitTracker(1); + typeTracker.track(dexSimple); + assertThat(typeTracker.outsideLimits()).isTrue(); + } + + @Test + public void testTrackerReuseAfterClear() throws Exception { + DexLimitTracker tracker = new DexLimitTracker(8); + Dex dexSimple = loadDexFromArchive(simpleDexArchive); + Dex dexFieldsTypes = loadDexFromArchive(fieldsTypesDexArchive); + + tracker.track(dexFieldsTypes); + assertThat(tracker.outsideLimits()).isTrue(); + + // Clear and reuse tracker instance + tracker.clear(); + assertThat(tracker.outsideLimits()).isFalse(); + + tracker.track(dexSimple); + assertThat(tracker.outsideLimits()).isFalse(); + } + + @Test + public void testClearInterner() throws Exception { + Dex dex1 = loadDexFromArchive(simpleDexArchive); + DexTrackerInfo info1 = DexTrackerInfo.create(dex1); + assertThat(info1.types).isNotEmpty(); + + // Calling clearInterner() clears the thread-local interner instance + DexLimitTracker.clearInterner(); + + Dex dex2 = loadDexFromArchive(simpleDexArchive); + DexTrackerInfo info2 = DexTrackerInfo.create(dex2); + assertThat(info2.types).isNotEmpty(); + } }