blob: 558825124bf152c9e5dc9ee5c2eb61a3888194e0 [file] [log] [blame]
/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.devrel.gmscore.tools.apk.arsc;
import com.google.common.base.Preconditions;
import com.google.devrel.gmscore.tools.apk.arsc.ArscBlamer.ResourceEntry;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* Calculates extra information about an {@link ArscBlamer.ResourceEntry}, such as the total
* APK size the entry is responsible for.
*
* This class is not thread-safe.
*/
public class ResourceEntryStatsCollector {
/** The size in bytes of an offset in a chunk. */
private static final int OFFSET_SIZE = 4;
/** The size in bytes of overhead for styles, if present, in {@link StringPoolChunk}. */
private static final int STYLE_OVERHEAD = 8;
/**
* The number of bytes, in addition to the header, that the {@link PackageChunk} has in overhead
* excluding the chunks it contains.
*/
private static final int PACKAGE_CHUNK_OVERHEAD = 8;
private final Map<ResourceEntry, ResourceStatistics> stats = new HashMap<>();
private final ArscBlamer blamer;
private final ResourceTableChunk resourceTable;
/**
* Creates a new {@link ResourceEntryStatsCollector}.
*
* @param blamer The blamer that maps resource entries to what they use.
* @param resourceTable The resource table that {@code blamer} is blamed on.
*/
public ResourceEntryStatsCollector(ArscBlamer blamer, ResourceTableChunk resourceTable) {
this.resourceTable = resourceTable;
this.blamer = blamer;
}
public void compute() throws IOException {
Preconditions.checkState(stats.isEmpty(), "Must only call #compute once.");
blamer.blame();
computeStringPoolSizes();
computePackageSizes();
}
/** Returns entries for which there are computed stats. Must first call {@link #compute}. */
public Map<ResourceEntry, ResourceStatistics> getStats() {
Preconditions.checkState(!stats.isEmpty(), "Must call #compute() first.");
return Collections.unmodifiableMap(stats);
}
/** Returns computed stats for a given entry. Must first call {@link #compute}. */
public ResourceStatistics getStats(ResourceEntry entry) {
Preconditions.checkState(!stats.isEmpty(), "Must call #compute() first.");
return stats.containsKey(entry) ? stats.get(entry) : ResourceStatistics.EMPTY;
}
private void computeStringPoolSizes() throws IOException {
computePoolSizes(resourceTable.getStringPool(), blamer.getStringToBlamedResources());
}
private void computePackageSizes() throws IOException {
computeTypePoolSizes();
computeKeyPoolSizes();
computeTypeSpecSizes();
computeTypeChunkSizes();
computePackageChunkSizes();
}
private void computeTypePoolSizes() throws IOException {
for (Entry<PackageChunk, List<ResourceEntry>[]> entry
: blamer.getTypeToBlamedResources().entrySet()) {
computePoolSizes(entry.getKey().getTypeStringPool(), entry.getValue());
}
}
private void computeKeyPoolSizes() throws IOException {
for (Entry<PackageChunk, List<ResourceEntry>[]> entry
: blamer.getKeyToBlamedResources().entrySet()) {
computePoolSizes(entry.getKey().getKeyStringPool(), entry.getValue());
}
}
private void computeTypeSpecSizes() {
for (Entry<PackageChunk, List<ResourceEntry>[]> entry
: blamer.getTypeToBlamedResources().entrySet()) {
computeTypeSpecSizes(entry.getKey(), entry.getValue());
}
}
private void computeTypeChunkSizes() {
for (Entry<TypeChunk.Entry, Collection<ResourceEntry>> entry
: blamer.getTypeEntryToBlamedResources().asMap().entrySet()) {
TypeChunk.Entry chunkEntry = entry.getKey();
TypeChunk typeChunk = chunkEntry.parent();
int size = chunkEntry.size() + OFFSET_SIZE;
int count = typeChunk.getEntries().size();
int nullEntries = typeChunk.getTotalEntryCount() - typeChunk.getEntries().size();
int overhead = typeChunk.getHeaderSize() + nullEntries * OFFSET_SIZE;
addSizes(entry.getValue(), overhead, size, count);
}
}
private void computePackageChunkSizes() {
for (Entry<PackageChunk, Collection<ResourceEntry>> entry
: blamer.getPackageToBlamedResources().asMap().entrySet()) {
int overhead = entry.getKey().getHeaderSize() + PACKAGE_CHUNK_OVERHEAD;
addSizes(entry.getValue(), overhead, 0, 1);
}
}
private void computePoolSizes(StringPoolChunk stringPool,
List<ResourceEntry>[] usages) throws IOException {
int overhead = stringPool.getHeaderSize();
if (stringPool.getStyleCount() > 0) {
overhead += STYLE_OVERHEAD;
}
// We have to iterate over the indices of the string pool, because it is possible that there are
// indices which have *no* associated resource entry (i.e. references from XML files without an
// entry in R).
int count = 0;
for (int i = 0; i < usages.length; ++i) {
if (usages[i].isEmpty()) {
overhead += computeStringAndStyleSize(stringPool, i);
} else {
++count;
}
}
// Now that we know the number of actual entries, we can compute the size.
for (int i = 0; i < usages.length; ++i) {
if (usages[i].isEmpty()) {
continue;
}
int size = computeStringAndStyleSize(stringPool, i);
addSizes(usages[i], overhead, size, count);
}
}
private void computeTypeSpecSizes(PackageChunk packageChunk,
List<ResourceEntry>[] usages) {
for (int i = 0; i < usages.length; ++i) {
// The 1 here is to convert back to a 1-based index.
TypeSpecChunk typeSpec = packageChunk.getTypeSpecChunk(i + 1);
// TypeSpecChunk entries share everything equally.
addSizes(usages[i], typeSpec.getOriginalChunkSize(), 0, 1);
}
}
/**
* Given an {@code index} into a {@code stringPool}, return string's total size in bytes plus its
* style, if any.
*
* @param stringPool The string pool containing the {@code index}.
* @param index The (0-based) index of the string and (optional) style.
* @throws IOException Thrown if the style's length could not be computed.
*/
private int computeStringAndStyleSize(StringPoolChunk stringPool, int index)
throws IOException {
return computeStringSize(stringPool, index) + computeStyleSize(stringPool, index);
}
/** Given an {@code index} into a {@code stringPool}, return string's total size in bytes. */
private int computeStringSize(StringPoolChunk stringPool, int index) {
String string = stringPool.getString(index);
int result = BinaryResourceString.encodeString(string, stringPool.getStringType()).length;
result += OFFSET_SIZE;
return result;
}
/**
* Given an {@code index} into a {@code stringPool}, return style's total size in bytes or 0 if
* there's no style at that index.
*
* @throws IOException Thrown if the style's length could not be computed.
*/
private int computeStyleSize(StringPoolChunk stringPool, int index) throws IOException {
if (index >= stringPool.getStyleCount()) { // No style at index
return 0;
}
return stringPool.getStyle(index).toByteArray().length + OFFSET_SIZE;
}
/**
* Adds to the {@link #stats} of {@code entries} that reference a value in a chunk the bytes it's
* responsible for. This should only be called once per chunk-value pair.
*
* @param entries The resource entries referencing a single value in a chunk.
* @param overhead The number of bytes of overhead of a chunk. Typically the header size.
* @param size The size in bytes of a value in a chunk that {@code entries} reference.
* @param count The total number of values in the chunk.
*/
private void addSizes(Collection<ResourceEntry> entries, int overhead, int size, int count) {
int usageCount = entries.size();
for (ResourceEntry resourceEntry : entries) {
// TODO(acornwall): Replace with Java 8's #getOrDefault when possible.
if (!stats.containsKey(resourceEntry)) {
stats.put(resourceEntry, new ResourceStatistics());
}
ResourceStatistics resourceStats = stats.get(resourceEntry);
if (usageCount == 1) {
resourceStats.addPrivateSize(size);
} else {
resourceStats.addSharedSize(size);
}
// Special case: If the chunk only has one relevant value, removing this entry will remove the
// entire chunk.
if (usageCount == 1 && count == 1) {
resourceStats.addPrivateSize(overhead);
}
resourceStats.addProportionalSize(size, usageCount);
resourceStats.addProportionalSize(overhead, usageCount * count);
}
}
/** Stats for an individual {@link ArscBlamer.ResourceEntry}. */
public static class ResourceStatistics {
/** The empty, immutable instance of ResourceStatistics which contains 0 for all values. */
public static final ResourceStatistics EMPTY = new ResourceStatistics();
private int privateSize = 0;
private int sharedSize = 0;
private double proportionalSize = 0;
private ResourceStatistics() {}
/** The number of bytes that would be freed if this resource was removed. */
public int getPrivateSize() {
return privateSize;
}
/** The number of bytes taken up by this resource that are also shared with other resources. */
public int getSharedSize() {
return sharedSize;
}
/** The total size this resource is responsible for. */
public double getProportionalSize() {
return proportionalSize;
}
private void addPrivateSize(int privateSize) {
this.privateSize += privateSize;
}
private void addSharedSize(int sharedSize) {
this.sharedSize += sharedSize;
}
private void addProportionalSize(int numerator, int denominator) {
this.proportionalSize += 1.0 * numerator / denominator;
}
}
}