Adds HeapReports library into perflib.
See go/heapreports for more detail on HeapReports.
Change-Id: I499acf9e00f8d7b103911573a6ca9e4fbe505567
diff --git a/perflib/build.gradle b/perflib/build.gradle
index d88f50f..d59b582 100644
--- a/perflib/build.gradle
+++ b/perflib/build.gradle
@@ -13,6 +13,7 @@
testCompile 'org.easymock:easymock:3.1'
testCompile 'junit:junit:4.12'
testCompile project(':base:testutils')
+ testCompile 'org.mockito:mockito-all:1.9.5'
}
project.ext.pomName = 'Android Tools perflib'
diff --git a/perflib/perflib.iml b/perflib/perflib.iml
index 5dc20f5..79ab1df 100644
--- a/perflib/perflib.iml
+++ b/perflib/perflib.iml
@@ -13,5 +13,6 @@
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" name="Trove4j" level="project" />
<orderEntry type="module" module-name="testutils" scope="TEST" />
+ <orderEntry type="library" scope="TEST" name="mockito" level="project" />
</component>
-</module>
\ No newline at end of file
+</module>
diff --git a/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/DefaultReport.java b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/DefaultReport.java
new file mode 100644
index 0000000..b4a701a
--- /dev/null
+++ b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/DefaultReport.java
@@ -0,0 +1,58 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.annotations.NonNull;
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.List;
+
+/**
+ * The default Report implementation, which prints the Report Name in the format "MyTaskName
+ * Report", the description (from {@link MemoryAnalyzerTask#getTaskDescription()}), and the table of
+ * results.
+ */
+public final class DefaultReport implements Report {
+
+ private final MemoryAnalyzerTask mTask;
+ protected List<AnalysisResultEntry<?>> mResults;
+
+ @VisibleForTesting
+ protected static final String NO_ISSUES_FOUND_STRING = "No issues found.";
+
+ public DefaultReport(@NonNull MemoryAnalyzerTask task) {
+ mTask = task;
+ }
+
+ @Override
+ public void generate(@NonNull List<AnalysisResultEntry<?>> data) {
+ mResults = data;
+ }
+
+ /**
+ * Prints the title of the report, the description, and the warning messages of all of the
+ * entries.
+ *
+ * <p>This simple output is sufficient for many tasks, assuming their titles and warning
+ * messages contain useful data.
+ *
+ * @param printer the {@link Printer} to print out to.
+ */
+ @Override
+ public void print(@NonNull Printer printer) {
+ printer.addHeading(
+ 2, (new StringBuilder()).append(mTask.getTaskName()).append(" Report").toString());
+
+ printer.addParagraph(mTask.getTaskDescription());
+
+ if (mResults != null && mResults.isEmpty()) {
+ printer.addParagraph(NO_ISSUES_FOUND_STRING);
+ return;
+ }
+ printer.startTable();
+ for (AnalysisResultEntry<?> result : mResults) {
+ printer.addRow(result.getWarningMessage());
+ }
+ printer.endTable();
+ }
+
+}
diff --git a/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/DuplicatedStringsReport.java b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/DuplicatedStringsReport.java
new file mode 100644
index 0000000..fd2fe48
--- /dev/null
+++ b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/DuplicatedStringsReport.java
@@ -0,0 +1,73 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.annotations.NonNull;
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.heap.Instance;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A report to organize the results of perflib's {@link DuplicatedStringsAnalyzerTask}.
+ *
+ * <p>Specifically, this report sorts the results by the effective size of the duplicated string:
+ * the length of the string times the number of occurrences. Additionally, when printed, it creates
+ * a detailed table including the string value, number of occurrences, total consumed size, and an
+ * example duplicate instance.
+ */
+public final class DuplicatedStringsReport implements Report {
+
+ private List<AnalysisResultEntry<?>> mResults;
+
+ // Limits the number of characters of the duplicated string is shown.
+ private static final int MAX_VALUE_STRING_LENGTH = 100;
+
+ @Override
+ public void generate(@NonNull List<AnalysisResultEntry<?>> results) {
+ // Sort by how many bytes each set of duplicates is actually occupying.
+ Collections.sort(
+ results,
+ Collections.reverseOrder(
+ new Comparator<AnalysisResultEntry<?>>() {
+ @Override
+ public int compare(AnalysisResultEntry<?> o1,
+ AnalysisResultEntry<?> o2) {
+ return getConsumedBytes(o1) - getConsumedBytes(o2);
+ }
+ }));
+
+ mResults = results;
+ }
+
+ @Override
+ public void print(@NonNull Printer printer) {
+ DuplicatedStringsAnalyzerTask task = new DuplicatedStringsAnalyzerTask();
+ printer.addHeading(2, task.getTaskName() + " Report");
+ printer.addParagraph(task.getTaskDescription());
+
+ if (mResults == null || mResults.isEmpty()) {
+ printer.addParagraph("No issues found.");
+ return;
+ }
+
+ printer.startTable("Value", "Bytes", "Duplicates", "First Duplicate");
+ for (AnalysisResultEntry<?> entry : mResults) {
+ String value = entry.getOffender().getOffendingDescription();
+ if (value.length() > MAX_VALUE_STRING_LENGTH) {
+ value = value.substring(0, MAX_VALUE_STRING_LENGTH) + "...";
+ }
+ String consumedBytes = Integer.toString(getConsumedBytes(entry));
+ String duplicates = Integer.toString(entry.getOffender().getOffenders().size());
+ String instance =
+ printer.formatInstance((Instance) entry.getOffender().getOffenders().get(0));
+ printer.addRow(value, consumedBytes, duplicates, instance);
+ }
+ printer.endTable();
+ }
+
+ private int getConsumedBytes(@NonNull AnalysisResultEntry<?> entry) {
+ return entry.getOffender().getOffendingDescription().length()
+ * entry.getOffender().getOffenders().size();
+ }
+}
diff --git a/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/HeapReports.java b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/HeapReports.java
new file mode 100644
index 0000000..8b5198f
--- /dev/null
+++ b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/HeapReports.java
@@ -0,0 +1,69 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.annotations.NonNull;
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.captures.MemoryMappedFileBuffer;
+import com.android.tools.perflib.heap.Snapshot;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility class which uses perflib to analyze Android heap dumps and creates {@link Report}s with
+ * the results.
+ */
+public final class HeapReports {
+
+ private HeapReports() {
+ }
+
+ /**
+ * Runs a {@link MemoryAnalyzerTask} on a {@link Snapshot} and returns a {@link DefaultReport}
+ * with the results.
+ *
+ * <p>The user is responsible for setting up the Snapshot as they need, for example by calling
+ * computeDominators() or resolveClasses() on the Snapshot. It is recommended to at least call
+ * computeDominators(), as most tasks will depend on the data generated in this method.
+ *
+ * @param task the task which should be run and whose results should be put in the report
+ * @param snapshot the heap dump to run on.
+ * @return a {@link DefaultReport} with the results of the task.
+ */
+ public static DefaultReport generateReport(@NonNull MemoryAnalyzerTask task,
+ @NonNull Snapshot snapshot) {
+ DefaultReport report = new DefaultReport(task);
+ generateReport(report, task, snapshot);
+ return report;
+ }
+
+ /**
+ * Runs a {@link MemoryAnalyzerTask} on a {@link Snapshot} and pass data to a custom {@link
+ * Report}.
+ *
+ * <p>The user is responsible for setting up the Snapshot as they need, for example by calling
+ * computeDominators() or resolveClasses() on the Snapshot. It is recommended to at least call
+ * computeDominators(), as most tasks will depend on the data generated in this method.
+ *
+ * <p>It is the user's responsibility to ensure that the report matches the task; otherwise,
+ * this method's behavior is undefined. For example, if you pass a DuplicatedStringsReport, you
+ * need to pass a DuplicatedStringsAnalyzerTask.
+ *
+ * @param report an instance of the custom Report, which will be generated and ready to print
+ * when the method returns.
+ * @param task the task which should be run and whose results should be put in the report.
+ * @param snapshot the heap dump to run on.
+ */
+ public static void generateReport(@NonNull Report report, @NonNull MemoryAnalyzerTask task,
+ @NonNull Snapshot snapshot) {
+ Set<MemoryAnalyzerTask> tasks = new HashSet<>();
+ tasks.add(task);
+ List<AnalysisResultEntry<?>> results = TaskRunner.runTasks(tasks, snapshot);
+ report.generate(results);
+ }
+}
diff --git a/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/HtmlPrinter.java b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/HtmlPrinter.java
new file mode 100644
index 0000000..ad4e0bb
--- /dev/null
+++ b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/HtmlPrinter.java
@@ -0,0 +1,161 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.annotations.NonNull;
+import com.android.tools.perflib.heap.Instance;
+import com.google.common.escape.Escaper;
+import com.google.common.html.HtmlEscapers;
+
+import java.awt.Dimension;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.file.Path;
+import java.util.Base64;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Prints summary documents as HTML.
+ *
+ * <p>This printer is designed to match the output of ahat's AhatPrinter as closely as possible.
+ * Specifically, see formatInstance.
+ */
+public final class HtmlPrinter implements Printer {
+
+ private final PrintStream mOutStream;
+ private final Escaper mEscaper;
+
+ // Number of characters of a String instance that will be shown when calling formatInstance.
+ // Picked 100 characters to give context to the string, but to prevent long strings from taking
+ // too much space. Feel free to change this in the future.
+ private static final int MAX_PREVIEW_STRING_LENGTH = 100;
+
+ public HtmlPrinter(@NonNull Path path) throws FileNotFoundException {
+ this(new PrintStream(path.toFile()));
+ }
+
+ public HtmlPrinter(@NonNull PrintStream outStream) {
+ mOutStream = outStream;
+ mEscaper = HtmlEscapers.htmlEscaper();
+ }
+
+ @Override
+ public void addHeading(int level, @NonNull String content) {
+ mOutStream.printf("<h%d>%s</h%1$d>\n", level, mEscaper.escape(content));
+ }
+
+ @Override
+ public void addParagraph(@NonNull String content) {
+ mOutStream.printf("<p>%s</p>\n", mEscaper.escape(content));
+ }
+
+ @Override
+ public void startTable(@NonNull String... columnHeadings) {
+ mOutStream.printf("<table>\n");
+
+ if (columnHeadings.length > 0) {
+ mOutStream.printf("<tr style='border: 1px solid black;'>\n");
+ for (String column : columnHeadings) {
+ mOutStream.printf("<th style='border: 1px solid black;'>%s</th>\n",
+ mEscaper.escape(column));
+ }
+ mOutStream.printf("</tr>\n");
+ }
+ }
+
+ @Override
+ public void addRow(@NonNull String... values) {
+ mOutStream.printf("<tr>\n");
+ for (String value : values) {
+ mOutStream.printf("<td>%s</td>\n", mEscaper.escape(value));
+ }
+ mOutStream.printf("</tr>\n");
+ }
+
+ @Override
+ public void endTable() {
+ mOutStream.printf("</table>\n");
+ }
+
+ @Override
+ public void addImage(@NonNull Instance instance) {
+ if (!HprofBitmapProvider.canGetBitmapFromInstance(instance)) {
+ return;
+ }
+ try {
+ HprofBitmapProvider bitmapProvider = new HprofBitmapProvider(instance);
+ String configName = bitmapProvider.getBitmapConfigName();
+ int width = bitmapProvider.getDimension().width;
+ int height = bitmapProvider.getDimension().height;
+
+ byte[] raw = bitmapProvider.getPixelBytes(new Dimension());
+ int[] converted;
+ if ("\"ARGB_8888\"".equals(configName)) {
+ converted = new int[width * height];
+ for (int i = 0; i < converted.length; i++) {
+ converted[i] = (
+ (((int) raw[i * 4 + 3] & 0xFF) << 24)
+ + (((int) raw[i * 4 + 0] & 0xFF) << 16)
+ + (((int) raw[i * 4 + 1] & 0xFF) << 8)
+ + ((int) raw[i * 4 + 2] & 0xFF));
+ }
+ } else {
+ throw new Exception("RGB_565/ALPHA_8 conversion not implemented");
+ }
+
+ int imageType = -1;
+ switch (configName) {
+ case "\"ARGB_8888\"":
+ imageType = BufferedImage.TYPE_4BYTE_ABGR;
+ break;
+ case "\"RGB_565\"":
+ imageType = BufferedImage.TYPE_USHORT_565_RGB;
+ break;
+ case "\"ALPHA_8\"":
+ imageType = BufferedImage.TYPE_BYTE_GRAY;
+ break;
+ }
+
+ BufferedImage image = new BufferedImage(width, height, imageType);
+ image.setRGB(0, 0, width, height, converted, 0, width);
+
+ ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
+ ImageIO.write(image, "png", byteOutputStream);
+ byteOutputStream.flush();
+ String imageDataString =
+ Base64.getEncoder().encodeToString(byteOutputStream.toByteArray());
+ byteOutputStream.close();
+
+ mOutStream.printf("<img src='data:image/png;base64,%s' \\>\n", imageDataString);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return;
+ }
+ }
+
+ @Override
+ public String formatInstance(@NonNull Instance instance) {
+ return instance.toString();
+ }
+
+ /**
+ * Convert a {@link BufferedImage} into a Base64 string.
+ *
+ * @return the string, or null if an exception occurred.
+ */
+ private String bitmapAsBase64String(@NonNull BufferedImage image) {
+ try {
+ ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
+ ImageIO.write(image, "png", byteOutputStream);
+ byteOutputStream.flush();
+ String imageDataString =
+ Base64.getEncoder().encodeToString(byteOutputStream.toByteArray());
+ byteOutputStream.close();
+ return imageDataString;
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/Printer.java b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/Printer.java
new file mode 100644
index 0000000..f63353d
--- /dev/null
+++ b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/Printer.java
@@ -0,0 +1,36 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.annotations.NonNull;
+import com.android.tools.perflib.heap.Instance;
+
+/**
+ * Represents an object capable of printing documents.
+ */
+public interface Printer {
+
+ void addHeading(int level, @NonNull String content);
+
+ void addParagraph(@NonNull String content);
+
+ /**
+ * Start a table which will have the given column headings. If no column headings are supplied,
+ * the table will simply start with the first row of data. This should be followed by calls to
+ * addRow. Be sure to end the table with endTable().
+ */
+ void startTable(@NonNull String... columnHeadings);
+
+ void addRow(@NonNull String... values);
+
+ void endTable();
+
+ void addImage(@NonNull Instance instance);
+
+ /**
+ * Turn an {@link Instance} into a string and return it.
+ *
+ * <p>Some printers may apply special formatting when printing an Instance object; for example,
+ * the {@link HtmlPrinter} puts {@code instance.toString()} in bold. If you do not want your
+ * printer to apply any special format, simply return {@code instance.toString()}.
+ */
+ String formatInstance(@NonNull Instance instance);
+}
diff --git a/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/Report.java b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/Report.java
new file mode 100644
index 0000000..715c47a
--- /dev/null
+++ b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/Report.java
@@ -0,0 +1,30 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.annotations.NonNull;
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+
+import java.util.List;
+
+/**
+ * Formats a set of {@link AnalysisResultEntry}s in a human-readable way.
+ *
+ * <p>In their {@link #generate} method, Reports process a list of results of a perflib {@link
+ * MemoryAnalyzerTask}. In their {@link #print} method, Reports visualize the results through a
+ * printer interface. <p>{@link DefaultReport} is provided as a basic Report implementation.
+ */
+public interface Report {
+
+ /**
+ * Take {@link MemoryAnalyzerTask} results and process them - e.g. sort the results.
+ */
+ void generate(@NonNull List<AnalysisResultEntry<?>> data);
+
+ /**
+ * Print report to a {@link Printer}.
+ *
+ * <p>Please read the Printer interface documentation to see what methods are available when
+ * overriding print().
+ */
+ void print(@NonNull Printer printer);
+
+}
diff --git a/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/TaskRunner.java b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/TaskRunner.java
new file mode 100644
index 0000000..02f19cb
--- /dev/null
+++ b/perflib/src/main/java/com/android/tools/perflib/heap/memoryanalyzer/TaskRunner.java
@@ -0,0 +1,96 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.annotations.NonNull;
+import com.android.tools.perflib.analyzer.AnalysisReport.Listener;
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.analyzer.Capture;
+import com.android.tools.perflib.analyzer.CaptureGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Runs a set of {@link MemoryAnalyzerTask}s and returns the results.
+ */
+final class TaskRunner {
+
+ /**
+ * Blocks until the given tasks are run through a {@link MemoryAnalyzer}.
+ *
+ * <p>All result entries will be aggregated into one list and returned.
+ *
+ * @param captureGroup the perflib {@link CaptureGroup} to run the tasks on.
+ * @return list of results, or null if the report was cancelled or the tasks interrupted.
+ */
+ static List<AnalysisResultEntry<?>> runTasks(
+ @NonNull Set<MemoryAnalyzerTask> tasks, @NonNull Set<Listener> listeners,
+ @NonNull CaptureGroup captureGroup) {
+
+ final List<AnalysisResultEntry<?>> generatedEntries = new ArrayList<>();
+
+ final ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+ // Setup listeners - user supplied listeners from this.listeners, plus our own custom listener.
+ final Set<Listener> listenerSet = new HashSet<>();
+ listenerSet.addAll(listeners);
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicBoolean cancelledOrInterrupted = new AtomicBoolean(false);
+ listenerSet.add(
+ new Listener() {
+ @Override
+ public void onResultsAdded(List<AnalysisResultEntry<?>> entries) {
+ generatedEntries.addAll(entries);
+ }
+
+ @Override
+ public void onAnalysisComplete() {
+ latch.countDown();
+ }
+
+ @Override
+ public void onAnalysisCancelled() {
+ cancelledOrInterrupted.set(true);
+ latch.countDown();
+ }
+ });
+
+ MemoryAnalyzer memoryAnalyzer = new MemoryAnalyzer();
+ memoryAnalyzer.analyze(captureGroup, listenerSet, tasks, executorService, executorService);
+
+ // Block until complete.
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ cancelledOrInterrupted.set(true);
+ }
+
+ executorService.shutdownNow();
+
+ if (!cancelledOrInterrupted.get()) {
+ return generatedEntries;
+ } else {
+ return null;
+ }
+ }
+
+ static List<AnalysisResultEntry<?>> runTasks(
+ @NonNull Set<MemoryAnalyzerTask> tasks, @NonNull CaptureGroup captureGroup) {
+ return runTasks(tasks, Collections.emptySet(), captureGroup);
+ }
+
+ static List<AnalysisResultEntry<?>> runTasks(@NonNull Set<MemoryAnalyzerTask> tasks,
+ @NonNull Capture... captures) {
+ CaptureGroup captureGroup = new CaptureGroup();
+ for (Capture capture : captures) {
+ captureGroup.addCapture(capture);
+ }
+ return runTasks(tasks, Collections.emptySet(), captureGroup);
+ }
+}
diff --git a/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/BasicAnalyzerTask.java b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/BasicAnalyzerTask.java
new file mode 100644
index 0000000..94a832c
--- /dev/null
+++ b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/BasicAnalyzerTask.java
@@ -0,0 +1,52 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.heap.Snapshot;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dummy task for testing.
+ */
+class BasicAnalyzerTask extends MemoryAnalyzerTask {
+
+ protected static final String TASK_WARNING = "test warning";
+ protected static final String TEST_CATEGORY = "test category";
+ protected static final String TASK_NAME = "Basic Analyzer Task";
+ protected static final String TASK_DESCRIPTION = "Basic Analyzer Task";
+
+ @Override
+ protected List<AnalysisResultEntry<?>> analyze(Configuration configuration, Snapshot snapshot) {
+ List<AnalysisResultEntry<?>> list = new ArrayList<>();
+ list.add(new BasicResultEntry());
+ return list;
+ }
+
+ @Override
+ public String getTaskName() {
+ return TASK_NAME;
+ }
+
+ @Override
+ public String getTaskDescription() {
+ return TASK_DESCRIPTION;
+ }
+
+ static class BasicResultEntry extends MemoryAnalysisResultEntry {
+
+ protected BasicResultEntry() {
+ super(null, null);
+ }
+
+ @Override
+ public String getWarningMessage() {
+ return TASK_WARNING;
+ }
+
+ @Override
+ public String getCategory() {
+ return TEST_CATEGORY;
+ }
+ }
+}
diff --git a/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/DefaultReportTest.java b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/DefaultReportTest.java
new file mode 100644
index 0000000..1edce8a
--- /dev/null
+++ b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/DefaultReportTest.java
@@ -0,0 +1,66 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.heap.memoryanalyzer.BasicAnalyzerTask.BasicResultEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tests for {@link Report}.
+ */
+@RunWith(JUnit4.class)
+public class DefaultReportTest {
+
+ @Mock
+ private Printer mPrinterMock;
+
+ @Before
+ public void setUpMocks() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testPrintFormatting_dataEmpty() {
+ // arrange
+ List<AnalysisResultEntry<?>> data = Collections.emptyList();
+ Report report = new DefaultReport(new BasicAnalyzerTask());
+ report.generate(data);
+
+ // act
+ report.print(mPrinterMock);
+
+ // verify
+ Mockito.verify(mPrinterMock)
+ .addParagraph(DefaultReport.NO_ISSUES_FOUND_STRING);
+ }
+
+ @Test
+ public void testPrintFormatting_dataGenerated() {
+ // arrange
+ List<AnalysisResultEntry<?>> data = new ArrayList<>();
+ data.add(new BasicResultEntry());
+ Report report = new DefaultReport(new BasicAnalyzerTask());
+ report.generate(data);
+
+ // act
+ report.print(mPrinterMock);
+
+ // verify
+ Mockito.verify(mPrinterMock)
+ .addHeading(2, BasicAnalyzerTask.TASK_NAME + " Report");
+ Mockito.verify(mPrinterMock)
+ .addParagraph(BasicAnalyzerTask.TASK_DESCRIPTION);
+ Mockito.verify(mPrinterMock)
+ .addRow(BasicAnalyzerTask.TASK_WARNING);
+ }
+}
diff --git a/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/DuplicatedStringsReportTest.java b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/DuplicatedStringsReportTest.java
new file mode 100644
index 0000000..3a7580a
--- /dev/null
+++ b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/DuplicatedStringsReportTest.java
@@ -0,0 +1,98 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.heap.Instance;
+import com.android.tools.perflib.heap.memoryanalyzer.DuplicatedStringsAnalyzerTask.DuplicatedStringsEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tests for {@link DuplicatedStringsReport}.
+ */
+@RunWith(JUnit4.class)
+public final class DuplicatedStringsReportTest {
+
+ private DuplicatedStringsEntry mEntry1;
+ private DuplicatedStringsEntry mEntry2;
+ private Instance mMockInstance;
+
+ @Mock
+ private Printer mPrinterMock;
+
+ @Before
+ public void setupMocks() {
+ MockitoAnnotations.initMocks(this);
+
+ mMockInstance = Mockito.mock(Instance.class, Mockito.RETURNS_DEEP_STUBS);
+ Mockito.when(mMockInstance.toString()).thenReturn("mockInstance");
+
+ mEntry1 = Mockito.mock(DuplicatedStringsEntry.class, Mockito.RETURNS_DEEP_STUBS);
+ mEntry2 = Mockito.mock(DuplicatedStringsEntry.class, Mockito.RETURNS_DEEP_STUBS);
+ Mockito.when(mEntry1.getOffender().getOffendingDescription())
+ .thenReturn("offending string 1");
+ Mockito.when(mEntry2.getOffender().getOffendingDescription())
+ .thenReturn("offending string 2");
+ Mockito.when(mEntry1.getOffender().getOffenders().size()).thenReturn(1);
+ Mockito.when(mEntry2.getOffender().getOffenders().size()).thenReturn(2);
+ Mockito.when((Object) mEntry1.getOffender().getOffenders().get(0))
+ .thenReturn(mMockInstance);
+ Mockito.when((Object) mEntry2.getOffender().getOffenders().get(0))
+ .thenReturn(mMockInstance);
+ }
+
+ @Test
+ public void testDuplicatedStringsReport() throws Exception {
+ // arrange
+ List<AnalysisResultEntry<?>> entries = new ArrayList<>();
+ entries.add(mEntry1);
+ entries.add(mEntry2);
+
+ // act
+ DuplicatedStringsReport report = new DuplicatedStringsReport();
+ report.generate(entries);
+ report.print(mPrinterMock);
+
+ // verify
+ DuplicatedStringsAnalyzerTask task = new DuplicatedStringsAnalyzerTask();
+ InOrder inOrder = Mockito.inOrder(mPrinterMock);
+ inOrder.verify(mPrinterMock).addHeading(2, task.getTaskName() + " Report");
+ inOrder.verify(mPrinterMock).addParagraph(task.getTaskDescription());
+ // verify that the entries were sorted correctly
+ inOrder
+ .verify(mPrinterMock)
+ .addRow(Mockito.anyString(), Mockito.anyString(), Mockito.eq("2"),
+ Mockito.anyString());
+ inOrder
+ .verify(mPrinterMock)
+ .addRow(Mockito.anyString(), Mockito.anyString(), Mockito.eq("1"),
+ Mockito.anyString());
+ }
+
+ @Test
+ public void testPrintFormatting_dataEmpty() {
+ // arrange
+ DuplicatedStringsAnalyzerTask task = new DuplicatedStringsAnalyzerTask();
+ List<AnalysisResultEntry<?>> data = Collections.emptyList();
+ Report report = new DuplicatedStringsReport();
+ report.generate(data);
+
+ // act
+ report.print(mPrinterMock);
+
+ // verify
+ Mockito.verify(mPrinterMock).addHeading(2, task.getTaskName() + " Report");
+ Mockito.verify(mPrinterMock).addParagraph(task.getTaskDescription());
+ Mockito.verify(mPrinterMock).addParagraph(Mockito.contains("No issues found."));
+ }
+}
diff --git a/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/HeapReportsTest.java b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/HeapReportsTest.java
new file mode 100644
index 0000000..4e712ad
--- /dev/null
+++ b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/HeapReportsTest.java
@@ -0,0 +1,46 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.heap.Heap;
+import com.android.tools.perflib.heap.Snapshot;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Matchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link HeapReports}.
+ */
+@RunWith(JUnit4.class)
+public class HeapReportsTest {
+
+ @Mock
+ private Snapshot mSnapshotMock;
+ @Mock
+ private Report mReportMock;
+
+ @Before
+ public void setUpMocks() {
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mSnapshotMock.getTypeName()).thenReturn(Snapshot.TYPE_NAME);
+ Mockito.when(mSnapshotMock.getRepresentation(Snapshot.class)).thenReturn(mSnapshotMock);
+ Mockito.when(mSnapshotMock.getHeaps()).thenReturn(new ArrayList<Heap>());
+ }
+
+ @Test
+ public void analyzeGeneratesData() {
+ // act
+ HeapReports.generateReport(mReportMock, new BasicAnalyzerTask(), mSnapshotMock);
+
+ // assert
+ Mockito.verify(mReportMock).generate(Matchers.<List<AnalysisResultEntry<?>>>any());
+ }
+}
diff --git a/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/HtmlPrinterTest.java b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/HtmlPrinterTest.java
new file mode 100644
index 0000000..1e3f19c
--- /dev/null
+++ b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/HtmlPrinterTest.java
@@ -0,0 +1,153 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.ddmlib.BitmapDecoder;
+import com.android.tools.perflib.heap.ArrayInstance;
+import com.android.tools.perflib.heap.ClassInstance;
+import com.android.tools.perflib.heap.ClassObj;
+import com.android.tools.perflib.heap.Field;
+import com.android.tools.perflib.heap.Instance;
+import com.android.tools.perflib.heap.StackFrame;
+import com.android.tools.perflib.heap.StackTrace;
+import com.android.tools.perflib.heap.Type;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for {@link HtmlPrinter}.
+ */
+@RunWith(JUnit4.class)
+public final class HtmlPrinterTest {
+
+ @Mock
+ private Instance mInstanceMock;
+ @Mock
+ private ClassObj mClassObjMock;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private ClassInstance mBitmapClassInstanceMock;
+ @Mock
+ private ArrayInstance mBufferMock;
+
+ private ByteArrayOutputStream mByteStream;
+ private HtmlPrinter mPrinter;
+
+ @Before
+ public void setUp() {
+ mByteStream = new ByteArrayOutputStream();
+ mPrinter = new HtmlPrinter(new PrintStream((mByteStream)));
+
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testAddHeading() throws Exception {
+ // act
+ mPrinter.addHeading(1, "test heading");
+
+ // assert
+ String out = new String(mByteStream.toByteArray(), StandardCharsets.UTF_8);
+ assertEquals(out, "<h1>test heading</h1>\n");
+ }
+
+ @Test
+ public void testAddParagraph() throws Exception {
+ // act
+ mPrinter.addParagraph("test paragraph");
+
+ // assert
+ String out = new String(mByteStream.toByteArray(), StandardCharsets.UTF_8);
+ assertEquals(out, "<p>test paragraph</p>\n");
+ }
+
+ @Test
+ public void testTable() throws Exception {
+ // act
+ mPrinter.startTable("test row heading");
+ mPrinter.addRow("test data");
+ mPrinter.endTable();
+
+ // assert
+ String out = new String(mByteStream.toByteArray(), StandardCharsets.UTF_8);
+ assertEquals(out,
+ "<table>\n"
+ + "<tr style='border: 1px solid black;'>\n"
+ + "<th style='border: 1px solid black;'>test row heading</th>\n"
+ + "</tr>\n"
+ + "<tr>\n"
+ + "<td>test data</td>\n"
+ + "</tr>\n"
+ + "</table>\n");
+ }
+
+ @Test
+ public void testTable_noHeadings() throws Exception {
+ // act
+ mPrinter.startTable();
+ mPrinter.addRow("test data");
+ mPrinter.endTable();
+
+ // assert
+ String out = new String(mByteStream.toByteArray(), StandardCharsets.UTF_8);
+ assertEquals(out,
+ "<table>\n"
+ + "<tr>\n"
+ + "<td>test data</td>\n"
+ + "</tr>\n"
+ + "</table>\n");
+ }
+
+ @Test
+ public void testAddImage() throws Exception {
+ // arrange
+ Mockito.when(mBufferMock.getArrayType()).thenReturn(Type.BYTE);
+ Mockito.when(mBufferMock.asRawByteArray(Mockito.anyInt(), Mockito.anyInt()))
+ .thenReturn(new byte[]{0, 0, 0, 0});
+ Mockito.when(mBufferMock.getLength()).thenReturn(4);
+ List<ClassInstance.FieldValue> fields = new ArrayList<>();
+ fields.add(new ClassInstance.FieldValue(new Field(Type.OBJECT, "mBuffer"), mBufferMock));
+ fields.add(new ClassInstance.FieldValue(new Field(Type.BOOLEAN, "mIsMutable"),
+ new Boolean(true)));
+ fields.add(new ClassInstance.FieldValue(new Field(Type.INT, "mWidth"), new Integer(1)));
+ fields.add(new ClassInstance.FieldValue(new Field(Type.INT, "mHeight"), new Integer(1)));
+ ClassObj bitmapClassObj = new ClassObj(0L, new StackTrace(0, 0, new StackFrame[0]),
+ BitmapDecoder.BITMAP_FQCN, 0L);
+ Mockito.when(mBitmapClassInstanceMock.getClassObj()).thenReturn(bitmapClassObj);
+ Mockito.when(mBitmapClassInstanceMock.getValues()).thenReturn(fields);
+
+ // act
+ mPrinter.addImage(mBitmapClassInstanceMock);
+
+ // assert
+ String out = new String(mByteStream.toByteArray(), StandardCharsets.UTF_8);
+ System.err.println(out);
+ assertEquals(out,
+ "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAA"
+ + "C0lEQVR42mNgAAIAAAUAAen63NgAAAAASUVORK5CYII=' \\>\n");
+ }
+
+ @Test
+ public void testFormatInstance() throws Exception {
+ // arrange
+ Mockito.when(mInstanceMock.toString()).thenReturn("mock instance");
+
+ // act
+ String out = mPrinter.formatInstance(mInstanceMock);
+
+ // assert
+ assertEquals(out, "mock instance");
+ }
+}
diff --git a/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/TaskRunnerTest.java b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/TaskRunnerTest.java
new file mode 100644
index 0000000..744979c
--- /dev/null
+++ b/perflib/src/test/java/com/android/tools/perflib/heap/memoryanalyzer/TaskRunnerTest.java
@@ -0,0 +1,66 @@
+package com.android.tools.perflib.heap.memoryanalyzer;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.perflib.analyzer.AnalysisResultEntry;
+import com.android.tools.perflib.heap.Heap;
+import com.android.tools.perflib.heap.Snapshot;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tests for {@link TaskRunner}.
+ */
+@RunWith(JUnit4.class)
+public class TaskRunnerTest {
+
+ @Mock
+ private Snapshot mSnapshotMock;
+
+ @Before
+ public void setUpMocks() {
+ MockitoAnnotations.initMocks(this);
+ Mockito.when(mSnapshotMock.getTypeName()).thenReturn(Snapshot.TYPE_NAME);
+ Mockito.when(mSnapshotMock.getRepresentation(Snapshot.class)).thenReturn(mSnapshotMock);
+ Mockito.when(mSnapshotMock.getHeaps()).thenReturn(Collections.<Heap>emptyList());
+ }
+
+ @Test
+ public void runTasksShouldGenerateEntry() throws InterruptedException {
+ // arrange
+ Set<MemoryAnalyzerTask> tasks = new HashSet<>();
+ tasks.add(new BasicAnalyzerTask());
+
+ // act
+ List<AnalysisResultEntry<?>> content = TaskRunner.runTasks(tasks, mSnapshotMock);
+
+ // assert
+ assertEquals(1, content.size());
+ assertEquals(BasicAnalyzerTask.TASK_WARNING, content.get(0).getWarningMessage());
+ }
+
+ @Test
+ public void runTasksShouldGenerateEntries_multipleCapture() throws InterruptedException {
+ // arrange
+ Set<MemoryAnalyzerTask> tasks = new HashSet<>();
+ tasks.add(new BasicAnalyzerTask());
+ // act
+ List<AnalysisResultEntry<?>> content = TaskRunner
+ .runTasks(tasks, mSnapshotMock, mSnapshotMock);
+
+ // assert
+ assertEquals(2, content.size());
+ assertEquals(BasicAnalyzerTask.TASK_WARNING, content.get(0).getWarningMessage());
+ }
+}