blob: c9d42c854b5445f6fae93bb795e0f63b85a30130 [file] [log] [blame]
Adam Pardyl0f1b3d42019-08-19 15:24:11 +02001/*
2 * Copyright (C) 2019 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
17package com.android.server.protolog;
18
19import static com.android.server.protolog.ProtoLogFileProto.LOG;
20import static com.android.server.protolog.ProtoLogFileProto.MAGIC_NUMBER;
21import static com.android.server.protolog.ProtoLogFileProto.MAGIC_NUMBER_H;
22import static com.android.server.protolog.ProtoLogFileProto.MAGIC_NUMBER_L;
23import static com.android.server.protolog.ProtoLogFileProto.REAL_TIME_TO_ELAPSED_TIME_OFFSET_MILLIS;
24import static com.android.server.protolog.ProtoLogFileProto.VERSION;
25import static com.android.server.protolog.ProtoLogMessage.BOOLEAN_PARAMS;
26import static com.android.server.protolog.ProtoLogMessage.DOUBLE_PARAMS;
27import static com.android.server.protolog.ProtoLogMessage.ELAPSED_REALTIME_NANOS;
28import static com.android.server.protolog.ProtoLogMessage.MESSAGE_HASH;
29import static com.android.server.protolog.ProtoLogMessage.SINT64_PARAMS;
30import static com.android.server.protolog.ProtoLogMessage.STR_PARAMS;
31
32import android.annotation.Nullable;
33import android.os.ShellCommand;
34import android.os.SystemClock;
35import android.util.Slog;
36import android.util.proto.ProtoOutputStream;
37
38import com.android.internal.annotations.VisibleForTesting;
39import com.android.server.protolog.common.IProtoLogGroup;
40import com.android.server.protolog.common.LogDataType;
Winson Chungb53b30c2019-11-18 21:03:09 -080041import com.android.internal.util.TraceBuffer;
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020042import com.android.server.wm.ProtoLogGroup;
43
44import java.io.File;
45import java.io.IOException;
46import java.io.PrintWriter;
47import java.util.ArrayList;
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020048import java.util.IllegalFormatConversionException;
49import java.util.TreeMap;
50import java.util.stream.Collectors;
51
52
53/**
54 * A service for the ProtoLog logging system.
55 */
56public class ProtoLogImpl {
57 private static final TreeMap<String, IProtoLogGroup> LOG_GROUPS = new TreeMap<>();
58
Adrian Roos2ca5d862019-10-03 17:30:14 +020059 /**
60 * A runnable to update the cached output of {@link #isEnabled}.
61 *
62 * Must be invoked after every action that could change the result of {@link #isEnabled}, eg.
63 * starting / stopping proto log, or enabling / disabling log groups.
64 */
65 static Runnable sCacheUpdater = () -> { };
66
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020067 private static void addLogGroupEnum(IProtoLogGroup[] config) {
Adrian Roos2ca5d862019-10-03 17:30:14 +020068 for (IProtoLogGroup group : config) {
69 LOG_GROUPS.put(group.name(), group);
70 }
Adam Pardyl0f1b3d42019-08-19 15:24:11 +020071 }
72
73 static {
74 addLogGroupEnum(ProtoLogGroup.values());
75 }
76
77 /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
78 public static void d(IProtoLogGroup group, int messageHash, int paramsMask,
79 @Nullable String messageString,
80 Object... args) {
81 getSingleInstance()
82 .log(LogLevel.DEBUG, group, messageHash, paramsMask, messageString, args);
83 }
84
85 /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
86 public static void v(IProtoLogGroup group, int messageHash, int paramsMask,
87 @Nullable String messageString,
88 Object... args) {
89 getSingleInstance().log(LogLevel.VERBOSE, group, messageHash, paramsMask, messageString,
90 args);
91 }
92
93 /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
94 public static void i(IProtoLogGroup group, int messageHash, int paramsMask,
95 @Nullable String messageString,
96 Object... args) {
97 getSingleInstance().log(LogLevel.INFO, group, messageHash, paramsMask, messageString, args);
98 }
99
100 /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
101 public static void w(IProtoLogGroup group, int messageHash, int paramsMask,
102 @Nullable String messageString,
103 Object... args) {
104 getSingleInstance().log(LogLevel.WARN, group, messageHash, paramsMask, messageString, args);
105 }
106
107 /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
108 public static void e(IProtoLogGroup group, int messageHash, int paramsMask,
109 @Nullable String messageString,
110 Object... args) {
111 getSingleInstance()
112 .log(LogLevel.ERROR, group, messageHash, paramsMask, messageString, args);
113 }
114
115 /** Used by the ProtoLogTool, do not call directly - use {@code ProtoLog} class instead. */
116 public static void wtf(IProtoLogGroup group, int messageHash, int paramsMask,
117 @Nullable String messageString,
118 Object... args) {
119 getSingleInstance().log(LogLevel.WTF, group, messageHash, paramsMask, messageString, args);
120 }
121
Adam Pardyla4e572d2019-09-24 17:56:44 +0200122 /** Returns true iff logging is enabled for the given {@code IProtoLogGroup}. */
123 public static boolean isEnabled(IProtoLogGroup group) {
Adrian Roos11480542019-10-03 14:37:57 +0200124 return group.isLogToLogcat()
Adam Pardyla4e572d2019-09-24 17:56:44 +0200125 || (group.isLogToProto() && getSingleInstance().isProtoEnabled());
126 }
127
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200128 private static final int BUFFER_CAPACITY = 1024 * 1024;
129 private static final String LOG_FILENAME = "/data/misc/wmtrace/wm_log.pb";
130 private static final String VIEWER_CONFIG_FILENAME = "/system/etc/protolog.conf.json.gz";
131 private static final String TAG = "ProtoLog";
132 private static final long MAGIC_NUMBER_VALUE = ((long) MAGIC_NUMBER_H << 32) | MAGIC_NUMBER_L;
133 static final String PROTOLOG_VERSION = "1.0.0";
134
135 private final File mLogFile;
136 private final TraceBuffer mBuffer;
137 private final ProtoLogViewerConfigReader mViewerConfig;
138
139 private boolean mProtoLogEnabled;
140 private boolean mProtoLogEnabledLockFree;
141 private final Object mProtoLogEnabledLock = new Object();
142
143 private static ProtoLogImpl sServiceInstance = null;
144
145 /**
146 * Returns the single instance of the ProtoLogImpl singleton class.
147 */
148 public static synchronized ProtoLogImpl getSingleInstance() {
149 if (sServiceInstance == null) {
150 sServiceInstance = new ProtoLogImpl(new File(LOG_FILENAME), BUFFER_CAPACITY,
151 new ProtoLogViewerConfigReader());
152 }
153 return sServiceInstance;
154 }
155
156 @VisibleForTesting
157 public static synchronized void setSingleInstance(@Nullable ProtoLogImpl instance) {
158 sServiceInstance = instance;
159 }
160
161 @VisibleForTesting
162 public enum LogLevel {
163 DEBUG, VERBOSE, INFO, WARN, ERROR, WTF
164 }
165
166 /**
167 * Main log method, do not call directly.
168 */
169 @VisibleForTesting
170 public void log(LogLevel level, IProtoLogGroup group, int messageHash, int paramsMask,
171 @Nullable String messageString, Object[] args) {
172 if (group.isLogToProto()) {
173 logToProto(messageHash, paramsMask, args);
174 }
175 if (group.isLogToLogcat()) {
176 logToLogcat(group.getTag(), level, messageHash, messageString, args);
177 }
178 }
179
180 private void logToLogcat(String tag, LogLevel level, int messageHash,
181 @Nullable String messageString, Object[] args) {
182 String message = null;
183 if (messageString == null) {
184 messageString = mViewerConfig.getViewerString(messageHash);
185 }
186 if (messageString != null) {
187 try {
188 message = String.format(messageString, args);
189 } catch (IllegalFormatConversionException ex) {
190 Slog.w(TAG, "Invalid ProtoLog format string.", ex);
191 }
192 }
193 if (message == null) {
194 StringBuilder builder = new StringBuilder("UNKNOWN MESSAGE (" + messageHash + ")");
195 for (Object o : args) {
196 builder.append(" ").append(o);
197 }
198 message = builder.toString();
199 }
200 passToLogcat(tag, level, message);
201 }
202
203 /**
204 * SLog wrapper.
205 */
206 @VisibleForTesting
207 public void passToLogcat(String tag, LogLevel level, String message) {
208 switch (level) {
209 case DEBUG:
210 Slog.d(tag, message);
211 break;
212 case VERBOSE:
213 Slog.v(tag, message);
214 break;
215 case INFO:
216 Slog.i(tag, message);
217 break;
218 case WARN:
219 Slog.w(tag, message);
220 break;
221 case ERROR:
222 Slog.e(tag, message);
223 break;
224 case WTF:
225 Slog.wtf(tag, message);
226 break;
227 }
228 }
229
230 private void logToProto(int messageHash, int paramsMask, Object[] args) {
231 if (!isProtoEnabled()) {
232 return;
233 }
234 try {
235 ProtoOutputStream os = new ProtoOutputStream();
236 long token = os.start(LOG);
237 os.write(MESSAGE_HASH, messageHash);
238 os.write(ELAPSED_REALTIME_NANOS, SystemClock.elapsedRealtimeNanos());
239
Adam Pardyla4e572d2019-09-24 17:56:44 +0200240 if (args != null) {
241 int argIndex = 0;
242 ArrayList<Long> longParams = new ArrayList<>();
243 ArrayList<Double> doubleParams = new ArrayList<>();
244 ArrayList<Boolean> booleanParams = new ArrayList<>();
245 for (Object o : args) {
246 int type = LogDataType.bitmaskToLogDataType(paramsMask, argIndex);
247 try {
248 switch (type) {
249 case LogDataType.STRING:
250 os.write(STR_PARAMS, o.toString());
251 break;
252 case LogDataType.LONG:
253 longParams.add(((Number) o).longValue());
254 break;
255 case LogDataType.DOUBLE:
256 doubleParams.add(((Number) o).doubleValue());
257 break;
258 case LogDataType.BOOLEAN:
259 booleanParams.add((boolean) o);
260 break;
261 }
262 } catch (ClassCastException ex) {
263 // Should not happen unless there is an error in the ProtoLogTool.
264 os.write(STR_PARAMS, "(INVALID PARAMS_MASK) " + o.toString());
265 Slog.e(TAG, "Invalid ProtoLog paramsMask", ex);
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200266 }
Adam Pardyla4e572d2019-09-24 17:56:44 +0200267 argIndex++;
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200268 }
Adam Pardyla4e572d2019-09-24 17:56:44 +0200269 if (longParams.size() > 0) {
270 os.writePackedSInt64(SINT64_PARAMS,
271 longParams.stream().mapToLong(i -> i).toArray());
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200272 }
Adam Pardyla4e572d2019-09-24 17:56:44 +0200273 if (doubleParams.size() > 0) {
274 os.writePackedDouble(DOUBLE_PARAMS,
275 doubleParams.stream().mapToDouble(i -> i).toArray());
276 }
277 if (booleanParams.size() > 0) {
278 boolean[] arr = new boolean[booleanParams.size()];
279 for (int i = 0; i < booleanParams.size(); i++) {
280 arr[i] = booleanParams.get(i);
281 }
282 os.writePackedBool(BOOLEAN_PARAMS, arr);
283 }
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200284 }
285 os.end(token);
286 mBuffer.add(os);
287 } catch (Exception e) {
288 Slog.e(TAG, "Exception while logging to proto", e);
289 }
290 }
291
292
293 @VisibleForTesting
294 ProtoLogImpl(File file, int bufferCapacity, ProtoLogViewerConfigReader viewerConfig) {
295 mLogFile = file;
296 mBuffer = new TraceBuffer(bufferCapacity);
297 mViewerConfig = viewerConfig;
298 }
299
300 /**
301 * Starts the logging a circular proto buffer.
302 *
303 * @param pw Print writer
304 */
305 public void startProtoLog(@Nullable PrintWriter pw) {
306 if (isProtoEnabled()) {
307 return;
308 }
309 synchronized (mProtoLogEnabledLock) {
310 logAndPrintln(pw, "Start logging to " + mLogFile + ".");
311 mBuffer.resetBuffer();
312 mProtoLogEnabled = true;
313 mProtoLogEnabledLockFree = true;
314 }
Adrian Roos2ca5d862019-10-03 17:30:14 +0200315 sCacheUpdater.run();
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200316 }
317
318 /**
319 * Stops logging to proto.
320 *
321 * @param pw Print writer
322 * @param writeToFile If the current buffer should be written to disk or not
323 */
324 public void stopProtoLog(@Nullable PrintWriter pw, boolean writeToFile) {
325 if (!isProtoEnabled()) {
326 return;
327 }
328 synchronized (mProtoLogEnabledLock) {
329 logAndPrintln(pw, "Stop logging to " + mLogFile + ". Waiting for log to flush.");
330 mProtoLogEnabled = mProtoLogEnabledLockFree = false;
331 if (writeToFile) {
332 writeProtoLogToFileLocked();
333 logAndPrintln(pw, "Log written to " + mLogFile + ".");
334 }
335 if (mProtoLogEnabled) {
336 logAndPrintln(pw, "ERROR: logging was re-enabled while waiting for flush.");
337 throw new IllegalStateException("logging enabled while waiting for flush.");
338 }
339 }
Adrian Roos2ca5d862019-10-03 17:30:14 +0200340 sCacheUpdater.run();
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200341 }
342
343 /**
344 * Returns {@code true} iff logging to proto is enabled.
345 */
346 public boolean isProtoEnabled() {
347 return mProtoLogEnabledLockFree;
348 }
349
350 private int setLogging(ShellCommand shell, boolean setTextLogging, boolean value) {
351 String group;
352 while ((group = shell.getNextArg()) != null) {
353 IProtoLogGroup g = LOG_GROUPS.get(group);
354 if (g != null) {
355 if (setTextLogging) {
356 g.setLogToLogcat(value);
357 } else {
358 g.setLogToProto(value);
359 }
360 } else {
361 logAndPrintln(shell.getOutPrintWriter(), "No IProtoLogGroup named " + group);
362 return -1;
363 }
364 }
Adrian Roos2ca5d862019-10-03 17:30:14 +0200365 sCacheUpdater.run();
Adam Pardyl0f1b3d42019-08-19 15:24:11 +0200366 return 0;
367 }
368
369 private int unknownCommand(PrintWriter pw) {
370 pw.println("Unknown command");
371 pw.println("Window manager logging options:");
372 pw.println(" start: Start proto logging");
373 pw.println(" stop: Stop proto logging");
374 pw.println(" enable [group...]: Enable proto logging for given groups");
375 pw.println(" disable [group...]: Disable proto logging for given groups");
376 pw.println(" enable-text [group...]: Enable logcat logging for given groups");
377 pw.println(" disable-text [group...]: Disable logcat logging for given groups");
378 return -1;
379 }
380
381 /**
382 * Responds to a shell command.
383 */
384 public int onShellCommand(ShellCommand shell) {
385 PrintWriter pw = shell.getOutPrintWriter();
386 String cmd = shell.getNextArg();
387 if (cmd == null) {
388 return unknownCommand(pw);
389 }
390 switch (cmd) {
391 case "start":
392 startProtoLog(pw);
393 return 0;
394 case "stop":
395 stopProtoLog(pw, true);
396 return 0;
397 case "status":
398 logAndPrintln(pw, getStatus());
399 return 0;
400 case "enable":
401 return setLogging(shell, false, true);
402 case "enable-text":
403 mViewerConfig.loadViewerConfig(pw, VIEWER_CONFIG_FILENAME);
404 return setLogging(shell, true, true);
405 case "disable":
406 return setLogging(shell, false, false);
407 case "disable-text":
408 return setLogging(shell, true, false);
409 default:
410 return unknownCommand(pw);
411 }
412 }
413
414 /**
415 * Returns a human-readable ProtoLog status text.
416 */
417 public String getStatus() {
418 return "ProtoLog status: "
419 + ((isProtoEnabled()) ? "Enabled" : "Disabled")
420 + "\nEnabled log groups: \n Proto: "
421 + LOG_GROUPS.values().stream().filter(
422 it -> it.isEnabled() && it.isLogToProto())
423 .map(IProtoLogGroup::name).collect(Collectors.joining(" "))
424 + "\n Logcat: "
425 + LOG_GROUPS.values().stream().filter(
426 it -> it.isEnabled() && it.isLogToLogcat())
427 .map(IProtoLogGroup::name).collect(Collectors.joining(" "))
428 + "\nLogging definitions loaded: " + mViewerConfig.knownViewerStringsNumber();
429 }
430
431 /**
432 * Writes the log buffer to a new file for the bugreport.
433 *
434 * This method is synchronized with {@code #startProtoLog(PrintWriter)} and
435 * {@link #stopProtoLog(PrintWriter, boolean)}.
436 */
437 public void writeProtoLogToFile() {
438 synchronized (mProtoLogEnabledLock) {
439 writeProtoLogToFileLocked();
440 }
441 }
442
443 private void writeProtoLogToFileLocked() {
444 try {
445 long offset =
446 (System.currentTimeMillis() - (SystemClock.elapsedRealtimeNanos() / 1000000));
447 ProtoOutputStream proto = new ProtoOutputStream();
448 proto.write(MAGIC_NUMBER, MAGIC_NUMBER_VALUE);
449 proto.write(VERSION, PROTOLOG_VERSION);
450 proto.write(REAL_TIME_TO_ELAPSED_TIME_OFFSET_MILLIS, offset);
451 mBuffer.writeTraceToFile(mLogFile, proto);
452 } catch (IOException e) {
453 Slog.e(TAG, "Unable to write buffer to file", e);
454 }
455 }
456
457
458 static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
459 Slog.i(TAG, msg);
460 if (pw != null) {
461 pw.println(msg);
462 pw.flush();
463 }
464 }
465}
466