Emit compact ouput .dex files.
Change-Id: I69ca23b53a542db7e7a18d819795e795bf0822c0
http://b/3447216
diff --git a/dx/src/com/android/dx/dex/TableOfContents.java b/dx/src/com/android/dx/dex/TableOfContents.java
index a85e9bc..9865f1c 100644
--- a/dx/src/com/android/dx/dex/TableOfContents.java
+++ b/dx/src/com/android/dx/dex/TableOfContents.java
@@ -70,6 +70,7 @@
public void readFrom(DexBuffer buffer) throws IOException {
readHeader(buffer.open(0));
readMap(buffer.open(mapList.off));
+ computeSizesFromOffsets();
}
private void readHeader(DexBuffer.Section headerIn) throws UnsupportedEncodingException {
@@ -113,7 +114,6 @@
private void readMap(DexBuffer.Section in) throws IOException {
int mapSize = in.readInt();
-
Section previous = null;
for (int i = 0; i < mapSize; i++) {
short type = in.readShort();
@@ -130,22 +130,27 @@
section.size = size;
section.off = offset;
- if (previous != null) {
- if (previous.off > offset) {
- throw new DexException("Map is unsorted at 0x" + Integer.toHexString(type));
- }
- previous.byteCount = offset - previous.off;
+ if (previous != null && previous.off > section.off) {
+ throw new DexException("Map is unsorted at " + previous + ", " + section);
}
previous = section;
}
+ Arrays.sort(sections);
+ }
- if (previous != null) {
- int endOfData = dataOff + dataSize;
- if (previous.off > endOfData) {
- throw new DexException("Map disagrees with data section offset and size");
+ public void computeSizesFromOffsets() {
+ int end = dataOff + dataSize;
+ for (int i = sections.length - 1; i >= 0; i--) {
+ Section section = sections[i];
+ if (section.off == -1) {
+ continue;
}
- previous.byteCount = endOfData - previous.off;
+ if (section.off > end) {
+ throw new DexException("Map is unsorted at " + section);
+ }
+ section.byteCount = end - section.off;
+ end = section.off;
}
}
@@ -186,32 +191,24 @@
public void writeMap(DexBuffer.Section out) throws IOException {
int count = 0;
- for (Section s : sections) {
- if (s.size > 0) {
+ for (Section section : sections) {
+ if (section.size > 0) {
count++;
}
}
out.writeInt(count);
- for (Section s : sections) {
- if (s.size > 0) {
- if (false) {
- System.out.println("Writing section " + s
- + " type=0x" + Integer.toHexString(s.type)
- + " offset=0x" + Integer.toHexString(s.off)
- + " size=0x" + Integer.toHexString(s.size)
- );
- }
-
- out.writeShort(s.type);
+ for (Section section : sections) {
+ if (section.size > 0) {
+ out.writeShort(section.type);
out.writeShort((short) 0);
- out.writeInt(s.size);
- out.writeInt(s.off);
+ out.writeInt(section.size);
+ out.writeInt(section.off);
}
}
}
- public static class Section {
+ public static class Section implements Comparable<Section> {
public final short type;
public int size = -1;
public int off = -1;
@@ -220,5 +217,16 @@
public Section(int type) {
this.type = (short) type;
}
+
+ public int compareTo(Section section) {
+ if (off != section.off) {
+ return off < section.off ? -1 : 1;
+ }
+ return 0;
+ }
+
+ @Override public String toString() {
+ return String.format("Section[type=%#x,off=%#x,size=%#x]", type, off, size);
+ }
}
}
diff --git a/dx/src/com/android/dx/io/DexBuffer.java b/dx/src/com/android/dx/io/DexBuffer.java
index a281fe5..f9559ff 100644
--- a/dx/src/com/android/dx/io/DexBuffer.java
+++ b/dx/src/com/android/dx/io/DexBuffer.java
@@ -467,8 +467,8 @@
return result;
}
- private void checkPosition() {
- if (position > limit) {
+ private void ensureCapacity(int size) {
+ if (position + size > limit) {
throw new DexException("Section limit " + limit + " exceeded by " + name);
}
}
@@ -491,21 +491,21 @@
}
public void write(byte[] bytes) {
+ ensureCapacity(bytes.length);
System.arraycopy(bytes, 0, data, position, bytes.length);
position += bytes.length;
- checkPosition();
}
public void writeByte(int b) {
+ ensureCapacity(1);
data[position++] = (byte) b;
- checkPosition();
}
public void writeShort(short i) {
+ ensureCapacity(2);
data[position ] = (byte) i;
data[position + 1] = (byte) (i >>> 8);
position += 2;
- checkPosition();
}
public void write(short[] shorts) {
@@ -515,22 +515,22 @@
}
public void writeInt(int i) {
+ ensureCapacity(4);
data[position ] = (byte) i;
data[position + 1] = (byte) (i >>> 8);
data[position + 2] = (byte) (i >>> 16);
data[position + 3] = (byte) (i >>> 24);
position += 4;
- checkPosition();
}
public void writeUleb128(int i) {
position += Leb128Utils.writeUnsignedLeb128(data, position, i);
- checkPosition();
+ ensureCapacity(0);
}
public void writeSleb128(int i) {
position += Leb128Utils.writeSignedLeb128(data, position, i);
- checkPosition();
+ ensureCapacity(0);
}
public void writeStringData(String value) {
@@ -552,6 +552,13 @@
}
alignToFourBytes();
}
+
+ /**
+ * Returns the number of bytes remaining in this section.
+ */
+ public int remaining() {
+ return limit - position;
+ }
}
private static class DataInputStub implements DataInput {
diff --git a/dx/src/com/android/dx/merge/DexMerger.java b/dx/src/com/android/dx/merge/DexMerger.java
index d1dda94..c892c73 100644
--- a/dx/src/com/android/dx/merge/DexMerger.java
+++ b/dx/src/com/android/dx/merge/DexMerger.java
@@ -29,16 +29,15 @@
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
-import java.util.logging.Logger;
/**
* Combine two dex files into one.
*/
public final class DexMerger {
- private static final Logger logger = Logger.getLogger(DexMerger.class.getName());
-
+ private final WriterSizes writerSizes;
private final DexBuffer dexWriter = new DexBuffer();
private final DexBuffer.Section headerWriter;
+ /** All IDs and definitions sections */
private final DexBuffer.Section idsDefsWriter;
private final DexBuffer.Section mapListWriter;
private final DexBuffer.Section typeListWriter;
@@ -60,9 +59,19 @@
private final InstructionTransformer aInstructionTransformer;
private final InstructionTransformer bInstructionTransformer;
+ /** minimum number of wasted bytes before it's worthwhile to compact the result */
+ private int compactWasteThreshold = 1024 * 1024; // 1MiB
+ /** minimum number of wasted bytes before it's worthwhile to emit a warning. */
+ private final int warnWasteThreshold = 100 * 1024; // 100KiB
+
public DexMerger(DexBuffer dexA, DexBuffer dexB) throws IOException {
+ this(dexA, dexB, new WriterSizes(dexA, dexB));
+ }
+
+ private DexMerger(DexBuffer dexA, DexBuffer dexB, WriterSizes writerSizes) throws IOException {
this.dexA = dexA;
this.dexB = dexB;
+ this.writerSizes = writerSizes;
TableOfContents aContents = dexA.getTableOfContents();
TableOfContents bContents = dexB.getTableOfContents();
@@ -71,100 +80,67 @@
aInstructionTransformer = new InstructionTransformer(aIndexMap);
bInstructionTransformer = new InstructionTransformer(bIndexMap);
- headerWriter = dexWriter.appendSection(SizeOf.HEADER_ITEM, "header");
+ headerWriter = dexWriter.appendSection(writerSizes.header, "header");
+ idsDefsWriter = dexWriter.appendSection(writerSizes.idsDefs, "ids defs");
- // All IDs and definitions sections
- int idsDefsMaxSize
- = (aContents.stringIds.size + bContents.stringIds.size) * SizeOf.STRING_ID_ITEM
- + (aContents.typeIds.size + bContents.typeIds.size) * SizeOf.TYPE_ID_ITEM
- + (aContents.protoIds.size + bContents.protoIds.size) * SizeOf.PROTO_ID_ITEM
- + (aContents.fieldIds.size + bContents.fieldIds.size) * SizeOf.MEMBER_ID_ITEM
- + (aContents.methodIds.size + bContents.methodIds.size) * SizeOf.MEMBER_ID_ITEM
- + (aContents.classDefs.size + bContents.classDefs.size) * SizeOf.CLASS_DEF_ITEM;
- idsDefsWriter = dexWriter.appendSection(idsDefsMaxSize, "ids defs");
-
- // data section
contentsOut = dexWriter.getTableOfContents();
contentsOut.dataOff = dexWriter.getLength();
contentsOut.mapList.off = dexWriter.getLength();
contentsOut.mapList.size = 1;
- mapListWriter = dexWriter.appendSection(SizeOf.UINT
- + (contentsOut.sections.length * SizeOf.MAP_ITEM), "map list");
-
- /*
- * TODO: several of these sections are far too large than they need to be.
- *
- * classDataWriter: uleb references to code items are larger than
- * expected. We should use old & new code_item section offsets to
- * pick an appropriate blow up size
- *
- * stringDataWriter: this shouldn't have to be larger, but it is
- *
- * encodedArrayWriter: this shouldn't have to be larger, but it is
- */
+ mapListWriter = dexWriter.appendSection(writerSizes.mapList, "map list");
contentsOut.typeLists.off = dexWriter.getLength();
contentsOut.typeLists.size = 0;
- int maxTypeListBytes = aContents.typeLists.byteCount + bContents.typeLists.byteCount;
- typeListWriter = dexWriter.appendSection(maxTypeListBytes, "type list");
+ typeListWriter = dexWriter.appendSection(writerSizes.typeList, "type list");
contentsOut.annotationSetRefLists.off = dexWriter.getLength();
contentsOut.annotationSetRefLists.size = 0;
- annotationSetRefListWriter = dexWriter.appendSection(SizeOf.UINT, "annotation set ref list");
+ annotationSetRefListWriter = dexWriter.appendSection(
+ writerSizes.annotationSetRefList, "annotation set ref list");
contentsOut.annotationSets.off = dexWriter.getLength();
contentsOut.annotationSets.size = 0;
- annotationSetWriter = dexWriter.appendSection(SizeOf.UINT, "annotation set");
+ annotationSetWriter = dexWriter.appendSection(writerSizes.annotationSet, "annotation set");
contentsOut.classDatas.off = dexWriter.getLength();
contentsOut.classDatas.size = 0;
- int maxClassDataBytes = aContents.classDatas.byteCount + bContents.classDatas.byteCount;
- classDataWriter = dexWriter.appendSection(maxClassDataBytes * 2, "class data");
+ classDataWriter = dexWriter.appendSection(writerSizes.classData, "class data");
contentsOut.codes.off = dexWriter.getLength();
contentsOut.codes.size = 0;
- int maxCodeBytes = aContents.codes.byteCount + bContents.codes.byteCount;
- codeWriter = dexWriter.appendSection(maxCodeBytes, "code");
+ codeWriter = dexWriter.appendSection(writerSizes.code, "code");
contentsOut.stringDatas.off = dexWriter.getLength();
contentsOut.stringDatas.size = 0;
- int maxStringDataBytes = aContents.stringDatas.byteCount
- + bContents.stringDatas.byteCount;
- stringDataWriter = dexWriter.appendSection(maxStringDataBytes * 2, "string data");
+ stringDataWriter = dexWriter.appendSection(writerSizes.stringData, "string data");
contentsOut.debugInfos.off = dexWriter.getLength();
contentsOut.debugInfos.size = 0;
- int maxDebugInfoBytes = aContents.debugInfos.byteCount + bContents.debugInfos.byteCount;
- debugInfoWriter = dexWriter.appendSection(maxDebugInfoBytes, "debug info");
+ debugInfoWriter = dexWriter.appendSection(writerSizes.debugInfo, "debug info");
contentsOut.annotations.off = dexWriter.getLength();
contentsOut.annotations.size = 0;
- int maxAnnotationBytes = aContents.annotations.byteCount
- + bContents.annotations.byteCount;
- annotationWriter = dexWriter.appendSection(maxAnnotationBytes, "annotation");
+ annotationWriter = dexWriter.appendSection(writerSizes.annotation, "annotation");
contentsOut.encodedArrays.off = dexWriter.getLength();
contentsOut.encodedArrays.size = 0;
- int maxEncodedArrayBytes = aContents.encodedArrays.byteCount
- + bContents.encodedArrays.byteCount;
- encodedArrayWriter = dexWriter.appendSection(
- maxEncodedArrayBytes * 2, "encoded array");
+ encodedArrayWriter = dexWriter.appendSection(writerSizes.encodedArray, "encoded array");
contentsOut.annotationsDirectories.off = dexWriter.getLength();
contentsOut.annotationsDirectories.size = 0;
- int maxAnnotationsDirectoryBytes = aContents.annotationsDirectories.byteCount
- + bContents.annotationsDirectories.byteCount;
annotationsDirectoryWriter = dexWriter.appendSection(
- maxAnnotationsDirectoryBytes, "annotations");
+ writerSizes.annotationsDirectory, "annotations directory");
dexWriter.noMoreSections();
contentsOut.dataSize = dexWriter.getLength() - contentsOut.dataOff;
}
- public DexBuffer merge() throws IOException {
- long start = System.nanoTime();
+ public void setCompactWasteThreshold(int compactWasteThreshold) {
+ this.compactWasteThreshold = compactWasteThreshold;
+ }
+ private DexBuffer mergeDexBuffers() throws IOException {
mergeStringIds();
mergeTypeIds();
mergeTypeLists();
@@ -177,24 +153,52 @@
contentsOut.header.off = 0;
contentsOut.header.size = 1;
contentsOut.fileSize = dexWriter.getLength();
+ contentsOut.computeSizesFromOffsets();
contentsOut.writeHeader(headerWriter);
contentsOut.writeMap(mapListWriter);
- // close (and flush) the result, then reopen to generate and write the hashes
+ // generate and write the hashes
new DexHasher().writeHashes(dexWriter);
+ return dexWriter;
+ }
+
+ public DexBuffer merge() throws IOException {
+ long start = System.nanoTime();
+ DexBuffer result = mergeDexBuffers();
+
+ /*
+ * We use pessimistic sizes when merging dex files. If those sizes
+ * result in too many bytes wasted, compact the result. To compact,
+ * simply merge the result with itself.
+ */
+ WriterSizes compactedSizes = writerSizes.clone();
+ compactedSizes.minusWaste(this);
+ int wastedByteCount = writerSizes.size() - compactedSizes.size();
+ if (wastedByteCount > + compactWasteThreshold) {
+ DexMerger compacter = new DexMerger(dexWriter, dexWriter, compactedSizes);
+ result = compacter.mergeDexBuffers();
+ System.out.printf("Result compacted from %.1fKiB to %.1fKiB to save %.1fKiB%n",
+ dexWriter.getLength() / 1024f,
+ result.getLength() / 1024f,
+ wastedByteCount / 1024f);
+ } else if (wastedByteCount >= warnWasteThreshold) {
+ System.out.printf("Result includes %.1fKiB of wasted space",
+ wastedByteCount / 1024f);
+ }
+
long elapsed = System.nanoTime() - start;
- logger.info(String.format("Merged dex A (%d defs/%.1fKiB) with dex B "
- + "(%d defs/%.1fKiB). Result is %d defs/%.1fKiB. Took %.1fs",
+ System.out.printf("Merged dex A (%d defs/%.1fKiB) with dex B "
+ + "(%d defs/%.1fKiB). Result is %d defs/%.1fKiB. Took %.1fs%n",
dexA.getTableOfContents().classDefs.size,
dexA.getLength() / 1024f,
dexB.getTableOfContents().classDefs.size,
dexB.getLength() / 1024f,
- contentsOut.classDefs.size,
- dexWriter.getLength() / 1024f,
- elapsed / 1000000000f));
+ result.getTableOfContents().classDefs.size,
+ result.getLength() / 1024f,
+ elapsed / 1000000000f);
- return dexWriter;
+ return result;
}
/**
@@ -202,10 +206,16 @@
* merged dex file. Populates maps from old to new indices in the process.
*/
abstract class IdMerger<T extends Comparable<T>> {
+ private final DexBuffer.Section out;
+
+ protected IdMerger(DexBuffer.Section out) {
+ this.out = out;
+ }
+
public final void merge() {
TableOfContents.Section aSection = getSection(dexA.getTableOfContents());
TableOfContents.Section bSection = getSection(dexB.getTableOfContents());
- getSection(contentsOut).off = idsDefsWriter.getPosition();
+ getSection(contentsOut).off = out.getPosition();
DexBuffer.Section inA = dexA.open(aSection.off);
DexBuffer.Section inB = dexB.open(bSection.off);
@@ -265,7 +275,7 @@
}
private void mergeStringIds() {
- new IdMerger<String>() {
+ new IdMerger<String>(idsDefsWriter) {
@Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
return tableOfContents.stringIds;
}
@@ -287,7 +297,7 @@
}
private void mergeTypeIds() {
- new IdMerger<Integer>() {
+ new IdMerger<Integer>(idsDefsWriter) {
@Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
return tableOfContents.typeIds;
}
@@ -308,7 +318,7 @@
}
private void mergeProtoIds() {
- new IdMerger<ProtoId>() {
+ new IdMerger<ProtoId>(idsDefsWriter) {
@Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
return tableOfContents.protoIds;
}
@@ -328,7 +338,7 @@
}
private void mergeFieldIds() {
- new IdMerger<FieldId>() {
+ new IdMerger<FieldId>(idsDefsWriter) {
@Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
return tableOfContents.fieldIds;
}
@@ -348,7 +358,7 @@
}
private void mergeMethodIds() {
- new IdMerger<MethodId>() {
+ new IdMerger<MethodId>(idsDefsWriter) {
@Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
return tableOfContents.methodIds;
}
@@ -368,7 +378,7 @@
}
private void mergeTypeLists() {
- new IdMerger<TypeList>() {
+ new IdMerger<TypeList>(typeListWriter) {
@Override TableOfContents.Section getSection(TableOfContents tableOfContents) {
return tableOfContents.typeLists;
}
@@ -626,6 +636,95 @@
new EncodedValueTransformer(indexMap, in, encodedArrayWriter).transformArray();
}
+ /**
+ * Byte counts for the sections written when creating a dex. Target sizes
+ * are defined in one of two ways:
+ * <ul>
+ * <li>By pessimistically guessing how large the union of dex files will be.
+ * We're pessimistic because we can't predict the amount of duplication
+ * between dex files, nor can we predict the length of ULEB-encoded
+ * offsets or indices.
+ * <li>By exactly measuring an existing dex.
+ * </ul>
+ */
+ private static class WriterSizes implements Cloneable {
+ private int header = SizeOf.HEADER_ITEM;
+ private int idsDefs;
+ private int mapList;
+ private int typeList;
+ private int annotationSetRefList = SizeOf.UINT;
+ private int annotationSet = SizeOf.UINT;
+ private int classData;
+ private int code;
+ private int stringData;
+ private int debugInfo;
+ private int annotation;
+ private int encodedArray;
+ private int annotationsDirectory;
+
+ /**
+ * Compute sizes for merging a and b.
+ */
+ public WriterSizes(DexBuffer a, DexBuffer b) {
+ plus(a.getTableOfContents(), false);
+ plus(b.getTableOfContents(), false);
+ }
+
+ @Override public WriterSizes clone() {
+ try {
+ return (WriterSizes) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new AssertionError();
+ }
+ }
+
+ public void plus(TableOfContents contents, boolean exact) {
+ idsDefs += contents.stringIds.size * SizeOf.STRING_ID_ITEM
+ + contents.typeIds.size * SizeOf.TYPE_ID_ITEM
+ + contents.protoIds.size * SizeOf.PROTO_ID_ITEM
+ + contents.fieldIds.size * SizeOf.MEMBER_ID_ITEM
+ + contents.methodIds.size * SizeOf.MEMBER_ID_ITEM
+ + contents.classDefs.size * SizeOf.CLASS_DEF_ITEM;
+ mapList = SizeOf.UINT + (contents.sections.length * SizeOf.MAP_ITEM);
+ typeList += contents.typeLists.byteCount;
+ code += contents.codes.byteCount;
+ stringData += contents.stringDatas.byteCount;
+ debugInfo += contents.debugInfos.byteCount;
+ annotation += contents.annotations.byteCount;
+ annotationsDirectory += contents.annotationsDirectories.byteCount;
+
+ if (exact) {
+ classData += contents.classDatas.byteCount;
+ encodedArray += contents.encodedArrays.byteCount;
+ } else {
+ classData += (int) Math.ceil(contents.classDatas.byteCount * 1.34);
+ encodedArray += (contents.encodedArrays.byteCount * 2);
+ }
+ }
+
+ public void minusWaste(DexMerger dexMerger) {
+ header -= dexMerger.headerWriter.remaining();
+ idsDefs -= dexMerger.idsDefsWriter.remaining();
+ mapList -= dexMerger.mapListWriter.remaining();
+ typeList -= dexMerger.typeListWriter.remaining();
+ annotationSetRefList -= dexMerger.annotationSetRefListWriter.remaining();
+ annotationSet -= dexMerger.annotationSetWriter.remaining();
+ classData -= dexMerger.classDataWriter.remaining();
+ code -= dexMerger.codeWriter.remaining();
+ stringData -= dexMerger.stringDataWriter.remaining();
+ debugInfo -= dexMerger.debugInfoWriter.remaining();
+ annotation -= dexMerger.annotationWriter.remaining();
+ encodedArray -= dexMerger.encodedArrayWriter.remaining();
+ annotationsDirectory -= dexMerger.annotationsDirectoryWriter.remaining();
+ }
+
+ public int size() {
+ return header + idsDefs + mapList + typeList + annotationSetRefList + annotationSet
+ + classData + code + stringData + debugInfo + annotation + encodedArray
+ + annotationsDirectory;
+ }
+ }
+
public static void main(String[] args) throws IOException {
if (args.length != 3) {
printUsage();
diff --git a/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java b/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java
index 5b43455..58f4ffe 100644
--- a/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java
+++ b/dx/tests/115-merge/com/android/dx/merge/DexMergeTest.java
@@ -91,6 +91,39 @@
assertEquals(false, staticValues.getField("m").get(null));
}
+ /**
+ * Merging dex files uses pessimistic sizes that naturally leave gaps in the
+ * output files. If those gaps grow too large, the merger is supposed to
+ * compact the result. This exercises that by repeatedly merging a dex with
+ * itself.
+ */
+ public void testMergedOutputSizeIsBounded() throws Exception {
+ /*
+ * At the time this test was written, the output would grow ~25% with
+ * each merge. Setting a low 1KiB ceiling on the maximum size caused
+ * the file to be compacted every four merges.
+ */
+ int steps = 100;
+ int compactWasteThreshold = 1024;
+
+ DexBuffer dexA = new DexBuffer();
+ DexBuffer dexB = new DexBuffer();
+ dexA.loadFrom(resourceToFile("/testdata/Basic.dex"));
+ dexB.loadFrom(resourceToFile("/testdata/TryCatchFinally.dex"));
+ DexBuffer merged = new DexMerger(dexA, dexB).merge();
+
+ int maxLength = 0;
+ for (int i = 0; i < steps; i++) {
+ DexMerger dexMerger = new DexMerger(dexA, merged);
+ dexMerger.setCompactWasteThreshold(compactWasteThreshold);
+ merged = dexMerger.merge();
+ maxLength = Math.max(maxLength, merged.getLength());
+ }
+
+ int maxExpectedLength = dexA.getLength() + dexB.getLength() + compactWasteThreshold;
+ assertTrue(maxLength + " < " + maxExpectedLength, maxLength < maxExpectedLength);
+ }
+
public ClassLoader mergeAndLoad(String dexAResource, String dexBResource) throws IOException {
DexBuffer dexA = new DexBuffer();
DexBuffer dexB = new DexBuffer();