• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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