1 /*
2  * Copyright (C) 2023 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.microdroid.test.host;
18 
19 import static com.google.common.truth.Truth.assertWithMessage;
20 import static org.junit.Assert.assertNotNull;
21 
22 import com.android.microdroid.test.host.CommandRunner;
23 import com.android.tradefed.device.ITestDevice;
24 import com.android.tradefed.log.LogUtil.CLog;
25 import com.android.tradefed.util.SimpleStats;
26 
27 import java.io.File;
28 import java.io.FileReader;
29 import java.io.BufferedReader;
30 import java.text.ParseException;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import javax.annotation.Nonnull;
36 
37 /** This class provides utilities to interact with the hyp tracing subsystem */
38 public final class KvmHypTracer {
39 
40     private static final String HYP_TRACING_ROOT = "/sys/kernel/tracing/hyp/";
41     private static final String HYP_EVENTS[] = { "hyp_enter", "hyp_exit" };
42     private static final int DEFAULT_BUF_SIZE_KB = 4 * 1024;
43     private static final Pattern LOST_EVENT_PATTERN = Pattern.compile(
44             "^CPU:[0-9]* \\[LOST ([0-9]*) EVENTS\\]");
45     private static final Pattern EVENT_PATTERN = Pattern.compile(
46             "^\\[([0-9]*)\\][ \t]*([0-9]*\\.[0-9]*): (" + String.join("|", HYP_EVENTS) + ") (.*)");
47 
48     private final CommandRunner mRunner;
49     private final ITestDevice mDevice;
50     private final int mNrCpus;
51 
52     private final ArrayList<File> mTraces;
53 
setNode(String node, int val)54     private void setNode(String node, int val) throws Exception {
55         mRunner.run("echo " + val + " > " + HYP_TRACING_ROOT + node);
56     }
57 
eventDir(String event)58     private static String eventDir(String event) {
59         return "events/hyp/" + event + "/";
60     }
61 
isSupported(ITestDevice device)62     public static boolean isSupported(ITestDevice device) throws Exception {
63         for (String event: HYP_EVENTS) {
64             if (!device.doesFileExist(HYP_TRACING_ROOT + eventDir(event) + "/enable"))
65                 return false;
66         }
67         return true;
68     }
69 
KvmHypTracer(@onnull ITestDevice device)70     public KvmHypTracer(@Nonnull ITestDevice device) throws Exception {
71         assertWithMessage("Hypervisor tracing not supported")
72                 .that(isSupported(device)).isTrue();
73 
74         mDevice = device;
75         mRunner = new CommandRunner(mDevice);
76         mTraces = new ArrayList<File>();
77         mNrCpus = Integer.parseInt(mRunner.run("nproc"));
78     }
79 
run(String payload_cmd)80     public String run(String payload_cmd) throws Exception {
81         mTraces.clear();
82 
83         setNode("tracing_on", 0);
84         mRunner.run("echo 0 | tee " + HYP_TRACING_ROOT + "events/*/*/enable");
85         setNode("buffer_size_kb", DEFAULT_BUF_SIZE_KB);
86         for (String event: HYP_EVENTS)
87             setNode(eventDir(event) + "/enable", 1);
88         setNode("trace", 0);
89 
90         /* Cat each per-cpu trace_pipe in its own tmp file in the background */
91         String cmd = "cd " + HYP_TRACING_ROOT + ";";
92         String trace_pipes[] = new String[mNrCpus];
93         for (int i = 0; i < mNrCpus; i++) {
94             trace_pipes[i] = mRunner.run("mktemp -t trace_pipe.cpu" + i + ".XXXXXXXXXX");
95             cmd += "cat per_cpu/cpu" + i + "/trace_pipe > " + trace_pipes[i] + " &";
96             cmd += "CPU" + i + "_TRACE_PIPE_PID=$!;";
97         }
98 
99         /* Run the payload with tracing enabled */
100         cmd += "echo 1 > tracing_on;";
101         String cmd_stdout = mRunner.run("mktemp -t cmd_stdout.XXXXXXXXXX");
102         cmd += payload_cmd + " > " + cmd_stdout + ";";
103         cmd += "echo 0 > tracing_on;";
104 
105         /* Actively kill the cat subprocesses as trace_pipe is blocking */
106         for (int i = 0; i < mNrCpus; i++)
107             cmd += "kill -9 $CPU" + i + "_TRACE_PIPE_PID;";
108         cmd += "wait";
109 
110         /*
111          * The whole thing runs in a single command for simplicity as `adb
112          * shell` doesn't play well with subprocesses outliving their parent,
113          * and cat-ing a trace_pipe is blocking, so doing so from separate Java
114          * threads wouldn't be much easier as we would need to actively kill
115          * them too.
116          */
117         mRunner.run(cmd);
118 
119         for (String t: trace_pipes) {
120             File trace = mDevice.pullFile(t);
121             assertNotNull(trace);
122             mTraces.add(trace);
123             mRunner.run("rm -f " + t);
124         }
125 
126         String res = mRunner.run("cat " + cmd_stdout);
127         mRunner.run("rm -f " + cmd_stdout);
128         return res;
129     }
130 
getDurationStats()131     public SimpleStats getDurationStats() throws Exception {
132         SimpleStats stats = new SimpleStats();
133 
134         for (File trace: mTraces) {
135             BufferedReader br = new BufferedReader(new FileReader(trace));
136             double last = 0.0, hyp_enter = 0.0;
137             String l, prev_event = "";
138             while ((l = br.readLine()) != null) {
139                 Matcher matcher = LOST_EVENT_PATTERN.matcher(l);
140                 if (matcher.find())
141                     throw new OutOfMemoryError("Lost " + matcher.group(1) + " events");
142 
143                 matcher = EVENT_PATTERN.matcher(l);
144                 if (!matcher.find()) {
145                     CLog.w("Failed to parse hyp event: " + l);
146                     continue;
147                 }
148 
149                 int cpu = Integer.parseInt(matcher.group(1));
150                 if (cpu < 0 || cpu >= mNrCpus)
151                     throw new ParseException("Incorrect CPU number: " + cpu, 0);
152 
153                 double cur = Double.parseDouble(matcher.group(2));
154                 if (cur < last)
155                     throw new ParseException("Time must not go backward: " + cur, 0);
156                 last = cur;
157 
158                 String event = matcher.group(3);
159                 if (event.equals(prev_event)) {
160                     throw new ParseException("Hyp event found twice in a row: " + trace + " - " + l,
161                                              0);
162                 }
163 
164                 switch (event) {
165                     case "hyp_exit":
166                         if (prev_event.equals("hyp_enter"))
167                             stats.add(cur - hyp_enter);
168                         break;
169                     case "hyp_enter":
170                         hyp_enter = cur;
171                         break;
172                     default:
173                         throw new ParseException("Unexpected line in trace" + l, 0);
174                 }
175                 prev_event = event;
176             }
177         }
178 
179         return stats;
180     }
181 }
182