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