blob: 7107b74e078ab518741f332a0f3368bb039680ad [file] [log] [blame]
/*
* Copyright (C) 2015 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 com.android.traceur;
import android.system.Os;
import android.util.Log;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import perfetto.protos.DataSourceDescriptorOuterClass.DataSourceDescriptor;
import perfetto.protos.FtraceDescriptorOuterClass.FtraceDescriptor.AtraceCategory;
import perfetto.protos.TracingServiceStateOuterClass.TracingServiceState;
import perfetto.protos.TracingServiceStateOuterClass.TracingServiceState.DataSource;
/**
* Utility functions for calling Perfetto
*/
public class PerfettoUtils implements TraceUtils.TraceEngine {
static final String TAG = "Traceur";
public static final String NAME = "PERFETTO";
private static final String OUTPUT_EXTENSION = "perfetto-trace";
private static final String TEMP_DIR= "/data/local/traces/";
private static final String TEMP_TRACE_LOCATION = "/data/local/traces/.trace-in-progress.trace";
private static final String PERFETTO_TAG = "traceur";
private static final String MARKER = "PERFETTO_ARGUMENTS";
private static final int LIST_TIMEOUT_MS = 10000;
private static final int STARTUP_TIMEOUT_MS = 10000;
private static final int STOP_TIMEOUT_MS = 30000;
private static final long MEGABYTES_TO_BYTES = 1024L * 1024L;
private static final long MINUTES_TO_MILLISECONDS = 60L * 1000L;
// The total amount of memory allocated to the two target buffers will be divided according to a
// ratio of (BUFFER_SIZE_RATIO - 1) to 1.
private static final int BUFFER_SIZE_RATIO = 64;
// atrace trace categories that will result in added data sources in the Perfetto config.
private static final String CAMERA_TAG = "camera";
private static final String GFX_TAG = "gfx";
private static final String MEMORY_TAG = "memory";
private static final String POWER_TAG = "power";
private static final String SCHED_TAG = "sched";
private static final String WEBVIEW_TAG = "webview";
// Custom trace categories.
private static final String SYS_STATS_TAG = "sys_stats";
public String getName() {
return NAME;
}
public String getOutputExtension() {
return OUTPUT_EXTENSION;
}
public boolean traceStart(Collection<String> tags, int bufferSizeKb, boolean apps,
boolean attachToBugreport, boolean longTrace, int maxLongTraceSizeMb,
int maxLongTraceDurationMinutes) {
if (isTracingOn()) {
Log.e(TAG, "Attempting to start perfetto trace but trace is already in progress");
return false;
} else {
// If a temporary trace file already exists, save it before beginning a new trace.
File recoveredFile = TraceUtils.getOutputFile(
TraceUtils.getRecoveredFilename());
if (!traceDump(recoveredFile)) {
Log.w(TAG, "Failed to recover in-progress trace.");
}
}
// The user chooses a per-CPU buffer size due to atrace limitations.
// So we use this to ensure that we reserve the correctly-sized buffer.
int numCpus = Runtime.getRuntime().availableProcessors();
// Allots 1 / BUFFER_SIZE_RATIO to the small buffer and the remainder to the large buffer.
int totalBufferSizeKb = numCpus * bufferSizeKb;
int targetBuffer1Kb = totalBufferSizeKb / BUFFER_SIZE_RATIO;
int targetBuffer0Kb = totalBufferSizeKb - targetBuffer1Kb;
// Build the perfetto config that will be passed on the command line.
StringBuilder config = new StringBuilder()
.append("write_into_file: true\n")
// Ensure that we flush ftrace data every 30s even if cpus are idle.
.append("flush_period_ms: 30000\n");
// If the user has flagged that in-progress trace sessions should be grabbed
// during bugreports, and BetterBug is present.
if (attachToBugreport) {
config.append("bugreport_score: 500\n");
}
// Indicates that perfetto should notify Traceur if the tracing session's status
// changes.
config.append("notify_traceur: true\n");
if (longTrace) {
if (maxLongTraceSizeMb != 0) {
config.append("max_file_size_bytes: "
+ (maxLongTraceSizeMb * MEGABYTES_TO_BYTES) + "\n");
}
if (maxLongTraceDurationMinutes != 0) {
config.append("duration_ms: "
+ (maxLongTraceDurationMinutes * MINUTES_TO_MILLISECONDS)
+ "\n");
}
// Default value for long traces to write to file.
config.append("file_write_period_ms: 1000\n");
} else {
// For short traces, we don't write to the file.
// So, always use the maximum value here: 7 days.
config.append("file_write_period_ms: 604800000\n");
}
config.append("incremental_state_config {\n")
.append(" clear_period_ms: 15000\n")
.append("} \n")
// This is target_buffer: 0, which is used for ftrace and the ftrace-derived
// android.gpu.memory.
.append("buffers {\n")
.append(" size_kb: " + targetBuffer0Kb + "\n")
.append(" fill_policy: RING_BUFFER\n")
.append("} \n")
// This is target_buffer: 1, which is used for additional data sources.
.append("buffers {\n")
.append(" size_kb: " + targetBuffer1Kb + "\n")
.append(" fill_policy: RING_BUFFER\n")
.append("} \n")
.append("data_sources {\n")
.append(" config {\n")
.append(" name: \"linux.ftrace\"\n")
.append(" target_buffer: 0\n")
.append(" ftrace_config {\n")
.append(" symbolize_ksyms: true\n");
for (String tag : tags) {
// Tags are expected to be only letters, numbers, and underscores.
String cleanTag = tag.replaceAll("[^a-zA-Z0-9_]", "");
if (!cleanTag.equals(tag)) {
Log.w(TAG, "Attempting to use an invalid tag: " + tag);
}
config.append(" atrace_categories: \"" + cleanTag + "\"\n");
}
if (apps) {
config.append(" atrace_apps: \"*\"\n");
}
// Request a dense encoding of the common sched events (sched_switch, sched_waking).
if (tags.contains(SCHED_TAG)) {
config.append(" compact_sched {\n");
config.append(" enabled: true\n");
config.append(" }\n");
}
// These parameters affect only the kernel trace buffer size and how
// frequently it gets moved into the userspace buffer defined above.
config.append(" buffer_size_kb: 8192\n")
.append(" drain_period_ms: 1000\n")
.append(" }\n")
.append(" }\n")
.append("}\n")
.append(" \n");
// Captures initial counter values, updates are captured in ftrace.
if (tags.contains(MEMORY_TAG) || tags.contains(GFX_TAG)) {
config.append("data_sources: {\n")
.append(" config { \n")
.append(" name: \"android.gpu.memory\"\n")
.append(" target_buffer: 0\n")
.append(" }\n")
.append("}\n");
}
// For process association. If the memory tag is enabled,
// poll periodically instead of just once at the beginning.
config.append("data_sources {\n")
.append(" config {\n")
.append(" name: \"linux.process_stats\"\n")
.append(" target_buffer: 1\n");
if (tags.contains(MEMORY_TAG)) {
config.append(" process_stats_config {\n")
.append(" proc_stats_poll_ms: 60000\n")
.append(" }\n");
}
config.append(" }\n")
.append("} \n");
if (tags.contains(POWER_TAG)) {
config.append("data_sources: {\n")
.append(" config { \n")
.append(" name: \"android.power\"\n")
.append(" target_buffer: 1\n")
.append(" android_power_config {\n");
if (longTrace) {
config.append(" battery_poll_ms: 5000\n");
} else {
config.append(" battery_poll_ms: 1000\n");
}
config.append(" collect_power_rails: true\n")
.append(" battery_counters: BATTERY_COUNTER_CAPACITY_PERCENT\n")
.append(" battery_counters: BATTERY_COUNTER_CHARGE\n")
.append(" battery_counters: BATTERY_COUNTER_CURRENT\n")
.append(" }\n")
.append(" }\n")
.append("}\n");
}
if (tags.contains(SYS_STATS_TAG)) {
config.append("data_sources: {\n")
.append(" config { \n")
.append(" name: \"linux.sys_stats\"\n")
.append(" target_buffer: 1\n")
.append(" sys_stats_config {\n")
.append(" meminfo_period_ms: 1000\n")
.append(" vmstat_period_ms: 1000\n")
.append(" }\n")
.append(" }\n")
.append("}\n");
}
if (tags.contains(GFX_TAG)) {
config.append("data_sources: {\n")
.append(" config { \n")
.append(" name: \"android.surfaceflinger.frametimeline\"\n")
.append(" }\n")
.append("}\n");
}
if (tags.contains(CAMERA_TAG)) {
config.append("data_sources: {\n")
.append(" config { \n")
.append(" name: \"android.hardware.camera\"\n")
.append(" target_buffer: 1\n")
.append(" }\n")
.append("}\n");
}
// Also enable Chrome events when the WebView tag is enabled.
if (tags.contains(WEBVIEW_TAG)) {
String chromeTraceConfig = "{" +
"\\\"record_mode\\\":\\\"record-continuously\\\"," +
"\\\"included_categories\\\":[\\\"*\\\"]" +
"}";
config.append("data_sources: {\n")
.append(" config {\n")
.append(" name: \"org.chromium.trace_event\"\n")
.append(" chrome_config {\n")
.append(" trace_config: \"" + chromeTraceConfig + "\"\n")
.append(" }\n")
.append(" }\n")
.append("}\n")
.append("data_sources: {\n")
.append(" config {\n")
.append(" name: \"org.chromium.trace_metadata\"\n")
.append(" chrome_config {\n")
.append(" trace_config: \"" + chromeTraceConfig + "\"\n")
.append(" }\n")
.append(" }\n")
.append("}\n");
}
String configString = config.toString();
// If the here-doc ends early, within the config string, exit immediately.
// This should never happen.
if (configString.contains(MARKER)) {
throw new RuntimeException("The arguments to the Perfetto command are malformed.");
}
String cmd = "perfetto --detach=" + PERFETTO_TAG
+ " -o " + TEMP_TRACE_LOCATION
+ " -c - --txt"
+ " <<" + MARKER +"\n" + configString + "\n" + MARKER;
Log.v(TAG, "Starting perfetto trace.");
try {
Process process = TraceUtils.execWithTimeout(cmd, TEMP_DIR, STARTUP_TIMEOUT_MS);
if (process == null) {
return false;
} else if (process.exitValue() != 0) {
Log.e(TAG, "perfetto traceStart failed with: " + process.exitValue());
return false;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
Log.v(TAG, "perfetto traceStart succeeded!");
return true;
}
public void traceStop() {
Log.v(TAG, "Stopping perfetto trace.");
if (!isTracingOn()) {
Log.w(TAG, "No trace appears to be in progress. Stopping perfetto trace may not work.");
}
String cmd = "perfetto --stop --attach=" + PERFETTO_TAG;
try {
Process process = TraceUtils.execWithTimeout(cmd, null, STOP_TIMEOUT_MS);
if (process != null && process.exitValue() != 0) {
Log.e(TAG, "perfetto traceStop failed with: " + process.exitValue());
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public boolean traceDump(File outFile) {
traceStop();
// Short-circuit if a trace was not stopped.
if (isTracingOn()) {
Log.e(TAG, "Trace was not stopped successfully, aborting trace dump.");
return false;
}
// Short-circuit if the file we're trying to dump to doesn't exist.
if (!Files.exists(Paths.get(TEMP_TRACE_LOCATION))) {
Log.e(TAG, "In-progress trace file doesn't exist, aborting trace dump.");
return false;
}
Log.v(TAG, "Saving perfetto trace to " + outFile);
try {
Os.rename(TEMP_TRACE_LOCATION, outFile.getCanonicalPath());
} catch (Exception e) {
throw new RuntimeException(e);
}
outFile.setReadable(true, false); // (readable, ownerOnly)
outFile.setWritable(true, false); // (readable, ownerOnly)
return true;
}
public boolean isTracingOn() {
String cmd = "perfetto --is_detached=" + PERFETTO_TAG;
try {
Process process = TraceUtils.exec(cmd);
// 0 represents a detached process exists with this name
// 2 represents no detached process with this name
// 1 (or other error code) represents an error
int result = process.waitFor();
if (result == 0) {
return true;
} else if (result == 2) {
return false;
} else {
throw new RuntimeException("Perfetto error: " + result);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static TreeMap<String,String> perfettoListCategories() {
String cmd = "perfetto --query-raw";
Log.v(TAG, "Listing tags: " + cmd);
try {
TreeMap<String, String> result = new TreeMap<>();
// execWithTimeout() cannot be used because stdout must be consumed before the process
// is terminated.
Process perfetto = TraceUtils.exec(cmd, null, false);
TracingServiceState serviceState =
TracingServiceState.parseFrom(perfetto.getInputStream());
// Destroy the perfetto process if it times out.
if (!perfetto.waitFor(LIST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
Log.e(TAG, "perfettoListCategories timed out after " + LIST_TIMEOUT_MS + " ms.");
perfetto.destroyForcibly();
return result;
}
// The perfetto process completed and failed, but does not need to be destroyed.
if (perfetto.exitValue() != 0) {
Log.e(TAG, "perfettoListCategories failed with: " + perfetto.exitValue());
}
List<AtraceCategory> categories = null;
for (DataSource dataSource : serviceState.getDataSourcesList()) {
DataSourceDescriptor dataSrcDescriptor = dataSource.getDsDescriptor();
if (dataSrcDescriptor.getName().equals("linux.ftrace")){
categories = dataSrcDescriptor.getFtraceDescriptor().getAtraceCategoriesList();
break;
}
}
if (categories != null) {
for (AtraceCategory category : categories) {
result.put(category.getName(), category.getDescription());
}
}
return result;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}