1 /* 2 * Copyright (C) 2016 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 android.telecom.Logging; 18 19 import android.annotation.NonNull; 20 import android.telecom.Log; 21 import android.text.TextUtils; 22 import android.util.Pair; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.internal.util.IndentingPrintWriter; 26 27 import java.time.Instant; 28 import java.time.ZoneId; 29 import java.time.ZonedDateTime; 30 import java.time.format.DateTimeFormatter; 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.Comparator; 34 import java.util.HashMap; 35 import java.util.IllegalFormatException; 36 import java.util.LinkedList; 37 import java.util.List; 38 import java.util.Locale; 39 import java.util.Map; 40 import java.util.concurrent.LinkedBlockingQueue; 41 42 /** 43 * A utility class that provides the ability to define Events that a subsystem deems important, and 44 * then relate those events to other events so that information can be extracted. For example, a 45 * START and FINISH event can be defined and when a START and then FINISH occurs in a sequence, the 46 * time it took to complete that sequence can be saved to be retrieved later. 47 * @hide 48 */ 49 50 public class EventManager { 51 52 public static final String TAG = "Logging.Events"; 53 @VisibleForTesting 54 public static final int DEFAULT_EVENTS_TO_CACHE = 10; // Arbitrarily chosen. 55 public static final DateTimeFormatter DATE_TIME_FORMATTER = 56 DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); 57 58 public interface Loggable { 59 /** 60 * @return a unique String ID that will allow the Event to be recognized later in the logs. 61 */ getId()62 String getId(); 63 64 /** 65 * @return Formatted information about the state that will be printed out later in the logs. 66 */ getDescription()67 String getDescription(); 68 } 69 70 private final Map<Loggable, EventRecord> mCallEventRecordMap = new HashMap<>(); 71 private LinkedBlockingQueue<EventRecord> mEventRecords = 72 new LinkedBlockingQueue<>(DEFAULT_EVENTS_TO_CACHE); 73 74 private List<EventListener> mEventListeners = new ArrayList<>(); 75 76 public interface EventListener { 77 /** 78 * Notifies the implementation of this method that a new event record has been added. 79 * @param eventRecord Reference to the recently added EventRecord 80 */ eventRecordAdded(EventRecord eventRecord)81 void eventRecordAdded(EventRecord eventRecord); 82 } 83 84 private SessionManager.ISessionIdQueryHandler mSessionIdHandler; 85 /** 86 * Maps from request events to a list of possible response events. Used to track 87 * end-to-end timing for critical user-facing operations in Telecom. 88 */ 89 private final Map<String, List<TimedEventPair>> requestResponsePairs = new HashMap<>(); 90 91 private static final Object mSync = new Object(); 92 93 /** 94 * Stores the various events. 95 * Also stores all request-response pairs amongst the events. 96 */ 97 public static class TimedEventPair { 98 private static final long DEFAULT_TIMEOUT = 3000L; 99 100 String mRequest; 101 String mResponse; 102 String mName; 103 long mTimeoutMillis = DEFAULT_TIMEOUT; 104 TimedEventPair(String request, String response, String name)105 public TimedEventPair(String request, String response, String name) { 106 this.mRequest = request; 107 this.mResponse = response; 108 this.mName = name; 109 } 110 TimedEventPair(String request, String response, String name, long timeoutMillis)111 public TimedEventPair(String request, String response, String name, long timeoutMillis) { 112 this.mRequest = request; 113 this.mResponse = response; 114 this.mName = name; 115 this.mTimeoutMillis = timeoutMillis; 116 } 117 } 118 addRequestResponsePair(TimedEventPair p)119 public void addRequestResponsePair(TimedEventPair p) { 120 if (requestResponsePairs.containsKey(p.mRequest)) { 121 requestResponsePairs.get(p.mRequest).add(p); 122 } else { 123 ArrayList<TimedEventPair> responses = new ArrayList<>(); 124 responses.add(p); 125 requestResponsePairs.put(p.mRequest, responses); 126 } 127 } 128 129 public static class Event { 130 public String eventId; 131 public String sessionId; 132 public long time; 133 public Object data; 134 // String storing the date for display. This will be computed at the time/timezone when 135 // the event is recorded. 136 public final String timestampString; 137 Event(String eventId, String sessionId, long time, Object data)138 public Event(String eventId, String sessionId, long time, Object data) { 139 this.eventId = eventId; 140 this.sessionId = sessionId; 141 this.time = time; 142 timestampString = 143 ZonedDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneId.systemDefault()) 144 .format(DATE_TIME_FORMATTER); 145 this.data = data; 146 } 147 } 148 149 public class EventRecord { 150 public class EventTiming extends TimedEvent<String> { 151 public String name; 152 public long time; 153 EventTiming(String name, long time)154 public EventTiming(String name, long time) { 155 this.name = name; 156 this.time = time; 157 } 158 getKey()159 public String getKey() { 160 return name; 161 } 162 getTime()163 public long getTime() { 164 return time; 165 } 166 } 167 168 private class PendingResponse { 169 String requestEventId; 170 long requestEventTimeMillis; 171 long timeoutMillis; 172 String name; 173 PendingResponse(String requestEventId, long requestEventTimeMillis, long timeoutMillis, String name)174 public PendingResponse(String requestEventId, long requestEventTimeMillis, 175 long timeoutMillis, String name) { 176 this.requestEventId = requestEventId; 177 this.requestEventTimeMillis = requestEventTimeMillis; 178 this.timeoutMillis = timeoutMillis; 179 this.name = name; 180 } 181 } 182 183 private final List<Event> mEvents = Collections.synchronizedList(new LinkedList<>()); 184 private final Loggable mRecordEntry; 185 EventRecord(Loggable recordEntry)186 public EventRecord(Loggable recordEntry) { 187 mRecordEntry = recordEntry; 188 } 189 getRecordEntry()190 public Loggable getRecordEntry() { 191 return mRecordEntry; 192 } 193 addEvent(String event, String sessionId, Object data)194 public void addEvent(String event, String sessionId, Object data) { 195 mEvents.add(new Event(event, sessionId, System.currentTimeMillis(), data)); 196 Log.i("Event", "RecordEntry %s: %s, %s", mRecordEntry.getId(), event, data); 197 } 198 getEvents()199 public List<Event> getEvents() { 200 return new LinkedList<>(mEvents); 201 } 202 extractEventTimings()203 public List<EventTiming> extractEventTimings() { 204 if (mEvents == null) { 205 return Collections.emptyList(); 206 } 207 208 LinkedList<EventTiming> result = new LinkedList<>(); 209 Map<String, PendingResponse> pendingResponses = new HashMap<>(); 210 synchronized (mEvents) { 211 for (Event event : mEvents) { 212 if (requestResponsePairs.containsKey(event.eventId)) { 213 // This event expects a response, so add that expected response to the maps 214 // of pending events. 215 for (EventManager.TimedEventPair p : requestResponsePairs.get( 216 event.eventId)) { 217 pendingResponses.put(p.mResponse, new PendingResponse(event.eventId, 218 event.time, p.mTimeoutMillis, p.mName)); 219 } 220 } 221 222 PendingResponse pendingResponse = pendingResponses.remove(event.eventId); 223 if (pendingResponse != null) { 224 long elapsedTime = event.time - pendingResponse.requestEventTimeMillis; 225 if (elapsedTime < pendingResponse.timeoutMillis) { 226 result.add(new EventTiming(pendingResponse.name, elapsedTime)); 227 } 228 } 229 } 230 } 231 232 return result; 233 } 234 dump(IndentingPrintWriter pw)235 public void dump(IndentingPrintWriter pw) { 236 pw.print(mRecordEntry.getDescription()); 237 238 pw.increaseIndent(); 239 // Iterate over copy of events so that this doesn't hold the lock for too long. 240 for (Event event : getEvents()) { 241 pw.print(event.timestampString); 242 pw.print(" - "); 243 pw.print(event.eventId); 244 if (event.data != null) { 245 pw.print(" ("); 246 Object data = event.data; 247 248 if (data instanceof Loggable) { 249 // If the data is another Loggable, then change the data to the 250 // Entry's Event ID instead. 251 EventRecord record = mCallEventRecordMap.get(data); 252 if (record != null) { 253 data = "RecordEntry " + record.mRecordEntry.getId(); 254 } 255 } 256 257 pw.print(data); 258 pw.print(")"); 259 } 260 if (!TextUtils.isEmpty(event.sessionId)) { 261 pw.print(":"); 262 pw.print(event.sessionId); 263 } 264 pw.println(); 265 } 266 267 pw.println("Timings (average for this call, milliseconds):"); 268 pw.increaseIndent(); 269 Map<String, Double> avgEventTimings = EventTiming.averageTimings(extractEventTimings()); 270 List<String> eventNames = new ArrayList<>(avgEventTimings.keySet()); 271 Collections.sort(eventNames); 272 for (String eventName : eventNames) { 273 pw.printf("%s: %.2f\n", eventName, avgEventTimings.get(eventName)); 274 } 275 pw.decreaseIndent(); 276 pw.decreaseIndent(); 277 } 278 } 279 EventManager(@onNull SessionManager.ISessionIdQueryHandler l)280 public EventManager(@NonNull SessionManager.ISessionIdQueryHandler l) { 281 mSessionIdHandler = l; 282 } 283 event(Loggable recordEntry, String event, Object data)284 public void event(Loggable recordEntry, String event, Object data) { 285 String currentSessionID = mSessionIdHandler.getSessionId(); 286 287 if (recordEntry == null) { 288 Log.i(TAG, "Non-call EVENT: %s, %s", event, data); 289 return; 290 } 291 synchronized (mEventRecords) { 292 if (!mCallEventRecordMap.containsKey(recordEntry)) { 293 EventRecord newRecord = new EventRecord(recordEntry); 294 addEventRecord(newRecord); 295 } 296 297 EventRecord record = mCallEventRecordMap.get(recordEntry); 298 record.addEvent(event, currentSessionID, data); 299 } 300 } 301 event(Loggable recordEntry, String event, String format, Object... args)302 public void event(Loggable recordEntry, String event, String format, Object... args) { 303 String msg; 304 try { 305 msg = (args == null || args.length == 0) ? format 306 : String.format(Locale.US, format, args); 307 } catch (IllegalFormatException ife) { 308 Log.e(this, ife, "IllegalFormatException: formatString='%s' numArgs=%d", format, 309 args.length); 310 msg = format + " (An error occurred while formatting the message.)"; 311 } 312 313 event(recordEntry, event, msg); 314 } 315 dumpEvents(IndentingPrintWriter pw)316 public void dumpEvents(IndentingPrintWriter pw) { 317 pw.println("Historical Events:"); 318 pw.increaseIndent(); 319 for (EventRecord eventRecord : mEventRecords) { 320 eventRecord.dump(pw); 321 } 322 pw.decreaseIndent(); 323 } 324 325 /** 326 * Dumps events in a timeline format. 327 * @param pw The {@link IndentingPrintWriter} to output the timeline to. 328 * @hide 329 */ dumpEventsTimeline(IndentingPrintWriter pw)330 public void dumpEventsTimeline(IndentingPrintWriter pw) { 331 pw.println("Historical Events (sorted by time):"); 332 333 // Flatten event records out for sorting. 334 List<Pair<Loggable, Event>> events = new ArrayList<>(); 335 for (EventRecord er : mEventRecords) { 336 for (Event ev : er.getEvents()) { 337 events.add(new Pair<>(er.getRecordEntry(), ev)); 338 } 339 } 340 341 // Sort by event time. This might result in out-of-order seeming events if the timezone 342 // changes somewhere in the middle. 343 Comparator<Pair<Loggable, Event>> byEventTime = 344 Comparator.comparingLong(e -> e.second.time); 345 events.sort(byEventTime); 346 347 pw.increaseIndent(); 348 for (Pair<Loggable, Event> event : events) { 349 pw.print(event.second.timestampString); 350 pw.print(","); 351 pw.print(event.first.getId()); 352 pw.print(","); 353 pw.print(event.second.eventId); 354 pw.print(","); 355 pw.println(event.second.data); 356 } 357 pw.decreaseIndent(); 358 } 359 changeEventCacheSize(int newSize)360 public void changeEventCacheSize(int newSize) { 361 362 // Resize the event queue. 363 LinkedBlockingQueue<EventRecord> oldEventLog = mEventRecords; 364 mEventRecords = new LinkedBlockingQueue<>(newSize); 365 mCallEventRecordMap.clear(); 366 367 oldEventLog.forEach((newRecord -> { 368 Loggable recordEntry = newRecord.getRecordEntry(); 369 // Copy the existing queue into the new one. 370 // First remove the oldest entry if no new ones exist. 371 if (mEventRecords.remainingCapacity() == 0) { 372 EventRecord record = mEventRecords.poll(); 373 if (record != null) { 374 mCallEventRecordMap.remove(record.getRecordEntry()); 375 } 376 } 377 378 // Now add a new entry 379 mEventRecords.add(newRecord); 380 mCallEventRecordMap.put(recordEntry, newRecord); 381 382 // Don't worry about notifying mEventListeners, since we are just resizing the records. 383 })); 384 } 385 registerEventListener(EventListener e)386 public void registerEventListener(EventListener e) { 387 if (e != null) { 388 synchronized (mSync) { 389 mEventListeners.add(e); 390 } 391 } 392 } 393 394 @VisibleForTesting getEventRecords()395 public LinkedBlockingQueue<EventRecord> getEventRecords() { 396 return mEventRecords; 397 } 398 399 @VisibleForTesting getCallEventRecordMap()400 public Map<Loggable, EventRecord> getCallEventRecordMap() { 401 return mCallEventRecordMap; 402 } 403 addEventRecord(EventRecord newRecord)404 private void addEventRecord(EventRecord newRecord) { 405 Loggable recordEntry = newRecord.getRecordEntry(); 406 407 // First remove the oldest entry if no new ones exist. 408 if (mEventRecords.remainingCapacity() == 0) { 409 EventRecord record = mEventRecords.poll(); 410 if (record != null) { 411 mCallEventRecordMap.remove(record.getRecordEntry()); 412 } 413 } 414 415 // Now add a new entry 416 mEventRecords.add(newRecord); 417 mCallEventRecordMap.put(recordEntry, newRecord); 418 synchronized (mSync) { 419 for (EventListener l : mEventListeners) { 420 l.eventRecordAdded(newRecord); 421 } 422 } 423 } 424 } 425