1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.tradefed.util; 18 19 import com.android.loganalysis.util.config.Option; 20 import com.android.tradefed.device.ITestDevice; 21 import com.android.tradefed.invoker.tracing.CloseableTraceScope; 22 import com.android.tradefed.log.LogUtil.CLog; 23 24 import java.io.File; 25 import java.io.IOException; 26 import java.io.InputStream; 27 import java.io.OutputStream; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.HashSet; 31 import java.util.LinkedHashMap; 32 import java.util.List; 33 import java.util.Map; 34 import java.util.Set; 35 import java.util.concurrent.TimeUnit; 36 37 /** A utility class for recording perfetto trace on a {@link ITestDevice}. */ 38 public class PerfettoTraceRecorder { 39 40 private static final String TRACE_NAME_FORMAT = "device-trace_%s_"; 41 42 @Option( 43 name = "perfetto-executable", 44 description = "Perfetto script file which will be used to record trace.") 45 private File perfettoExecutable = null; 46 47 @Option(name = "output-path", description = "Path where the files will be saved.") 48 private String outputPath = System.getProperty("java.io.tmpdir"); 49 50 // A device-metadata map to store trace related metadata. 51 private Map<ITestDevice, DeviceTraceMetadata> deviceMetadataMap = new LinkedHashMap<>(); 52 // A list of device serials where trace has started. 53 private Set<String> deviceSerialsWithTrace = new HashSet<>(); 54 55 // Time in ms to wait for the perfettoScript to start 56 private static final long SCRIPT_START_TIMEOUT = 10 * 1000; 57 58 /** 59 * Starts recording perfetto trace on device. Must call {@link 60 * PerfettoTraceRecorder#stopTrace(ITestDevice)} afterwards to stop the trace recording. 61 * 62 * @param device A {@link ITestDevice} where trace will be recorded. 63 * @param extraConfigs A map of extra configs that needs to be added in the trace config file. 64 */ startTrace(ITestDevice device, Map<String, String> extraConfigs)65 public void startTrace(ITestDevice device, Map<String, String> extraConfigs) 66 throws IOException { 67 if (deviceMetadataMap.containsKey(device)) { 68 CLog.d( 69 "Already recording trace on %s in pid %s.", 70 device.getSerialNumber(), deviceMetadataMap.get(device).getProcess().pid()); 71 return; 72 } 73 // Stores metadata related to this trace 74 DeviceTraceMetadata deviceTraceMetadata = new DeviceTraceMetadata(); 75 76 // Get the perfetto executable 77 if (perfettoExecutable == null) { 78 perfettoExecutable = FileUtil.createTempFile("record_android_trace", ".txt"); 79 InputStream script = 80 PerfettoTraceRecorder.class.getResourceAsStream( 81 "/perfetto/record_android_trace"); 82 FileUtil.writeToFile(script, perfettoExecutable); 83 } 84 deviceTraceMetadata.setPerfettoScript(perfettoExecutable, false); 85 86 // Make the script executable 87 RunUtil.getDefault() 88 .runTimedCmd(10000, "chmod", "u+x", perfettoExecutable.getAbsolutePath()); 89 90 // Get the trace config file from resource 91 File traceConfigFile = FileUtil.createTempFile("trace_config", ".textproto"); 92 InputStream configStream = 93 PerfettoTraceRecorder.class.getResourceAsStream("/perfetto/trace_config.textproto"); 94 String configStr = StreamUtil.getStringFromStream(configStream); 95 // insert extra configs in the trace config file 96 if (extraConfigs != null) { 97 StringBuilder sb = new StringBuilder(); 98 for (Map.Entry<String, String> configKeyValue : extraConfigs.entrySet()) { 99 sb.append( 100 String.format( 101 "%s: %s\n", configKeyValue.getKey(), configKeyValue.getValue())); 102 } 103 String injectedStr = sb.toString(); 104 configStr = configStr.replace("# {injected_config}", injectedStr); 105 } 106 FileUtil.writeToFile(configStr, traceConfigFile); 107 108 deviceTraceMetadata.setTraceConfig(traceConfigFile, true); 109 110 File traceOutput = 111 FileUtil.createTempFile( 112 String.format(TRACE_NAME_FORMAT, device.getSerialNumber()), 113 ".perfetto-trace"); 114 deviceTraceMetadata.setTraceOutput(traceOutput, false); 115 116 // start trace 117 List<String> cmd = 118 Arrays.asList( 119 perfettoExecutable.getAbsolutePath(), 120 "-c", 121 traceConfigFile.getAbsolutePath(), 122 "-s", 123 device.getSerialNumber(), 124 "-o", 125 traceOutput.getAbsolutePath(), 126 "-n"); 127 Process process = 128 RunUtil.getDefault() 129 .runCmdInBackground( 130 cmd, 131 new OutputStream() { 132 public String output = ""; 133 134 @Override 135 public void write(int b) throws IOException { 136 output += b; 137 checkOutput(); 138 } 139 140 @Override 141 public void write(byte[] b) throws IOException { 142 write(b, 0, b.length); 143 } 144 145 @Override 146 public void write(byte[] b, int off, int len) 147 throws IOException { 148 output += new String(b, off, len); 149 checkOutput(); 150 } 151 152 private void checkOutput() { 153 if (output.contains("beginning of main")) { 154 deviceSerialsWithTrace.add(device.getSerialNumber()); 155 } 156 } 157 }); 158 try (CloseableTraceScope ignore = new CloseableTraceScope("perfetto-script-start-time")) { 159 long startTime = System.currentTimeMillis(); 160 while (!deviceSerialsWithTrace.contains(device.getSerialNumber()) 161 && System.currentTimeMillis() - startTime < SCRIPT_START_TIMEOUT) { 162 // wait until perfetto trace has started 163 RunUtil.getDefault().sleep(1 * 1000); 164 } 165 } 166 if (!deviceSerialsWithTrace.contains(device.getSerialNumber())) { 167 CLog.w( 168 "Perfetto script did not start on device %s within %sms. Trace file may miss" 169 + " some events.", 170 device.getSerialNumber(), SCRIPT_START_TIMEOUT); 171 } 172 deviceTraceMetadata.setProcess(process); 173 deviceMetadataMap.put(device, deviceTraceMetadata); 174 } 175 176 /** 177 * Stops recording perfetto trace on the device. 178 * 179 * <p>Must have called {@link PerfettoTraceRecorder#startTrace(ITestDevice, Map)} before. 180 * 181 * @param device device for which to stop the recording. @Return Returns the perfetto trace 182 * file. 183 */ stopTrace(ITestDevice device)184 public File stopTrace(ITestDevice device) { 185 if (deviceMetadataMap.containsKey(device)) { 186 // remove the metadata from the map so that a new trace can be started. 187 DeviceTraceMetadata metadata = deviceMetadataMap.remove(device); 188 deviceSerialsWithTrace.remove(device.getSerialNumber()); 189 CommandResult result = 190 RunUtil.getDefault() 191 .runTimedCmd( 192 10000, 193 "kill", 194 "-2", 195 String.valueOf(metadata.getProcess().pid())); 196 if (result.getStatus() != CommandStatus.SUCCESS) { 197 CLog.d(result.getStderr()); 198 return null; 199 } 200 // wait for the recorder to finish and pull the trace file 201 try { 202 // 10 second wait is arbitrary. Should be enough time to pull big trace files. 203 boolean terminated = 204 metadata.getProcess().waitFor(10 * 1000, TimeUnit.MILLISECONDS); 205 if (!terminated) { 206 CLog.d( 207 "Perfetto process did not finish collection within 10 seconds. Trace" 208 + " file may be empty."); 209 } 210 } catch (InterruptedException e) { 211 CLog.w(e); 212 } 213 metadata.cleanUp(); 214 return metadata.getTraceOutput(); 215 } 216 return null; 217 } 218 219 /** Stores metadata related to a trace running on an {@link ITestDevice}. */ 220 private class DeviceTraceMetadata { 221 // the process running the script 222 public Process process; 223 // config file which was used to collect trace 224 private File traceConfig; 225 // Output file where traces will be pulled after trace is stopped. 226 private File traceOutput; 227 // Script used to run the trace 228 private File perfettoScript; 229 230 private List<File> tempFiles = new ArrayList<>(); 231 getProcess()232 public Process getProcess() { 233 return process; 234 } 235 setProcess(Process process)236 public void setProcess(Process process) { 237 this.process = process; 238 } 239 getTraceConfig()240 public File getTraceConfig() { 241 return traceConfig; 242 } 243 setTraceConfig(File traceConfig, boolean needToDelete)244 public void setTraceConfig(File traceConfig, boolean needToDelete) { 245 this.traceConfig = traceConfig; 246 if (needToDelete) { 247 tempFiles.add(traceConfig); 248 } 249 } 250 getTraceOutput()251 public File getTraceOutput() { 252 return traceOutput; 253 } 254 setTraceOutput(File traceOutput, boolean needToDelete)255 public void setTraceOutput(File traceOutput, boolean needToDelete) { 256 this.traceOutput = traceOutput; 257 if (needToDelete) { 258 tempFiles.add(traceOutput); 259 } 260 } 261 getPerfettoScript()262 public File getPerfettoScript() { 263 return perfettoScript; 264 } 265 setPerfettoScript(File perfettoScript, boolean needToDelete)266 public void setPerfettoScript(File perfettoScript, boolean needToDelete) { 267 this.perfettoScript = perfettoScript; 268 if (needToDelete) { 269 tempFiles.add(perfettoScript); 270 } 271 } 272 cleanUp()273 public void cleanUp() { 274 for (File file : tempFiles) { 275 FileUtil.deleteFile(file); 276 } 277 } 278 } 279 } 280