AtraceLogger.java
/*
* Copyright (C) 2017 The Android Open Source Project
*
* 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 androidx.test.rule.logging;
import android.app.Instrumentation;
import android.app.UiAutomation;
import android.os.Build.VERSION;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import androidx.annotation.RequiresApi;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
/**
* Class contains helper methods to dump atrace info asynchronously while running the test case.
*
* <p>Supported only for minsdk version 21 and above because of UiAutomation#executeShellCommand
* availability.
*
* @deprecated unsupported. Consider running trace from host such as via Android Studio
*/
@RequiresApi(21)
@Deprecated
public class AtraceLogger {
private static final String ATRACE_START = "atrace --async_start -b %d -c %s";
private static final String ATRACE_DUMP = "atrace --async_dump -b %d -z %s";
private static final String ATRACE_STOP = "atrace --async_stop -b %d -z %s";
private static final String ATRACEHELPER_TAG = "AtraceLogger";
private static final String CATEGORY_SEPARATOR = " ";
private static final int BUFFER_SIZE = 8192;
private static volatile AtraceLogger loggerInstance;
private UiAutomation uiAutomation;
private String traceFileName;
private List<ByteArrayOutputStream> atraceDataList;
private Thread dumpThread;
private File destAtraceDirectory;
private boolean atraceRunning = false;
private IOException dumpIOException;
private AtraceLogger(Instrumentation instrumentation) {
uiAutomation = instrumentation.getUiAutomation();
}
/**
* To make sure only one instance of atrace logger is created.
*
* @param instrumentation Used to execute atrace shell commands
* @return instance of the AtraceLogger
*/
public static AtraceLogger getAtraceLoggerInstance(Instrumentation instrumentation) {
if (VERSION.SDK_INT < 21) {
throw new UnsupportedOperationException("AtraceLogger is only supported on APIs >= 21");
}
if (loggerInstance == null) {
synchronized (AtraceLogger.class) {
if (loggerInstance == null) {
loggerInstance = new AtraceLogger(instrumentation);
}
}
}
return loggerInstance;
}
/**
* Method to start atrace and dump the data at regular interval. Note : Trace info will not be
* captured during the dumping if there are multiple dumps between the atraceStart and atraceStop
*
* @param traceCategoriesSet Set of atrace categories (i.e atrace --list_categories)
* @param atraceBufferSize Size of the circular buffer in kb
* @param dumpIntervalSecs Periodic interval to dump data from circular buffer
* @param destDirectory Directory under which atrace logs are stored
* @param traceFileName is optional parameter.Atrace files are dumped under destDirectory.
* traceFileName will be indexed based on number of dumps between atraceStart and atraceStop
* under destDirectory. If traceFileName is null or empty "atrace" name will be used for
* indexing the files and stored under destDirectory
* @throws IOException
*/
public void atraceStart(
Set<String> traceCategoriesSet,
int atraceBufferSize,
int dumpIntervalSecs,
File destDirectory,
String traceFileName)
throws IOException {
if (atraceRunning) {
throw new IllegalStateException("Attempted multiple atrace start");
}
if (traceCategoriesSet.isEmpty()) {
throw new IllegalArgumentException("Empty categories. Should contain atleast one category");
}
if (destDirectory == null) {
throw new IllegalArgumentException("Destination directory cannot be null");
}
if (!destDirectory.exists() && !destDirectory.mkdirs()) {
throw new IOException("Unable to create the destination directory");
}
destAtraceDirectory = destDirectory;
StringBuffer traceCategoriesList = new StringBuffer();
for (String traceCategory : traceCategoriesSet) {
traceCategoriesList.append(traceCategory).append(CATEGORY_SEPARATOR);
}
if (traceFileName != null && !traceFileName.isEmpty()) {
this.traceFileName = traceFileName;
}
String startCommand =
String.format(ATRACE_START, atraceBufferSize, traceCategoriesList.toString());
/*
* Since execute shell command is not blocked until the command executes successfully,
* write the output to byte array to make sure atrace start command is completed before
* starting the test
*/
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try {
writeDataToByteStream(uiAutomation.executeShellCommand(startCommand), outStream);
} finally {
outStream.close();
}
atraceRunning = true;
dumpIOException = null;
atraceDataList = new ArrayList<ByteArrayOutputStream>();
DumpTraceRunnable dumpTraceRunnable =
new DumpTraceRunnable(traceCategoriesList.toString(), atraceBufferSize, dumpIntervalSecs);
dumpThread = new Thread(dumpTraceRunnable);
dumpThread.start();
dumpTraceRunnable.waitForStart();
}
/**
* Method to write data into byte array
*
* @param pfDescriptor Used to read the content returned by shell command
* @param outputStream Write the data to this output stream read from pfDescriptor
* @throws IOException
*/
private void writeDataToByteStream(
ParcelFileDescriptor pfDescriptor, ByteArrayOutputStream outputStream) throws IOException {
InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pfDescriptor);
try {
byte[] buffer = new byte[BUFFER_SIZE];
int length;
while ((length = inputStream.read(buffer)) >= 0) {
outputStream.write(buffer, 0, length);
}
} finally {
inputStream.close();
}
}
/**
* Method to stop the atrace and write the atrace data cached in byte array list to file.
*
* @throws IOException
* @throws InterruptedException
*/
public void atraceStop() throws IOException, InterruptedException {
if (!atraceRunning) {
throw new IllegalStateException(
"ATrace is not running currently. Start atrace before" + "stopping.");
}
try {
dumpThread.interrupt();
dumpThread.join();
if (dumpIOException != null) {
throw dumpIOException;
}
atraceWrite();
} finally {
for (ByteArrayOutputStream outStream : atraceDataList) {
outStream.close();
}
atraceRunning = false;
traceFileName = null;
}
}
/**
* Method to write atrace data buffered in byte array stream to file
*
* @throws IOException
*/
private void atraceWrite() throws IOException {
int count = 0;
for (ByteArrayOutputStream outStream : atraceDataList) {
// Indexing the files from 0..(atraceDataList.size)-1 based on number of dumps
File file = null;
if (traceFileName != null) {
file =
new File(destAtraceDirectory, String.format("%s-atrace-%d.txt", traceFileName, count));
} else {
file = new File(destAtraceDirectory, String.format("atrace-%d.txt", count));
}
OutputStream fileOutputStream = new FileOutputStream(file);
try {
fileOutputStream.write(outStream.toByteArray());
} finally {
fileOutputStream.close();
}
count++;
}
}
/*
* Thread class periodically dumps the atrace data into byte stream
*/
private class DumpTraceRunnable implements Runnable {
private String traceCategories;
private int bufferSize;
private int dumpIntervalInSecs;
private final CountDownLatch startLatch = new CountDownLatch(1);
DumpTraceRunnable(String traceCategories, int bufferSize, int dumpIntervalInSecs) {
this.traceCategories = traceCategories;
this.bufferSize = bufferSize;
this.dumpIntervalInSecs = dumpIntervalInSecs;
}
void waitForStart() {
try {
startLatch.await();
} catch (InterruptedException e) {
// ignore
}
}
@Override
public void run() {
startLatch.countDown();
try {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(dumpIntervalInSecs * 1000);
} catch (InterruptedException e) {
break;
}
String dumpCommand = String.format(ATRACE_DUMP, bufferSize, traceCategories);
// Dump into byte array and maintain in the list
long startTime = System.currentTimeMillis();
ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream();
writeDataToByteStream(uiAutomation.executeShellCommand(dumpCommand), byteArrayOutStream);
atraceDataList.add(byteArrayOutStream);
long endTime = System.currentTimeMillis();
Log.i(ATRACEHELPER_TAG, "Time taken by - DumpTraceRunnable " + (endTime - startTime));
}
String stopCommand = String.format(ATRACE_STOP, bufferSize, traceCategories);
ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream();
writeDataToByteStream(uiAutomation.executeShellCommand(stopCommand), byteArrayOutStream);
atraceDataList.add(byteArrayOutStream);
} catch (IOException ioe) {
dumpIOException = ioe;
}
}
}
}