• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = 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 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             for (Event event : mEvents) {
211                 if (requestResponsePairs.containsKey(event.eventId)) {
212                     // This event expects a response, so add that expected response to the maps
213                     // of pending events.
214                     for (EventManager.TimedEventPair p : requestResponsePairs.get(event.eventId)) {
215                         pendingResponses.put(p.mResponse, new PendingResponse(event.eventId,
216                                 event.time, p.mTimeoutMillis, p.mName));
217                     }
218                 }
219 
220                 PendingResponse pendingResponse = pendingResponses.remove(event.eventId);
221                 if (pendingResponse != null) {
222                     long elapsedTime = event.time - pendingResponse.requestEventTimeMillis;
223                     if (elapsedTime < pendingResponse.timeoutMillis) {
224                         result.add(new EventTiming(pendingResponse.name, elapsedTime));
225                     }
226                 }
227             }
228 
229             return result;
230         }
231 
dump(IndentingPrintWriter pw)232         public void dump(IndentingPrintWriter pw) {
233             pw.print(mRecordEntry.getDescription());
234 
235             pw.increaseIndent();
236             for (Event event : mEvents) {
237                 pw.print(event.timestampString);
238                 pw.print(" - ");
239                 pw.print(event.eventId);
240                 if (event.data != null) {
241                     pw.print(" (");
242                     Object data = event.data;
243 
244                     if (data instanceof Loggable) {
245                         // If the data is another Loggable, then change the data to the
246                         // Entry's Event ID instead.
247                         EventRecord record = mCallEventRecordMap.get(data);
248                         if (record != null) {
249                             data = "RecordEntry " + record.mRecordEntry.getId();
250                         }
251                     }
252 
253                     pw.print(data);
254                     pw.print(")");
255                 }
256                 if (!TextUtils.isEmpty(event.sessionId)) {
257                     pw.print(":");
258                     pw.print(event.sessionId);
259                 }
260                 pw.println();
261             }
262 
263             pw.println("Timings (average for this call, milliseconds):");
264             pw.increaseIndent();
265             Map<String, Double> avgEventTimings = EventTiming.averageTimings(extractEventTimings());
266             List<String> eventNames = new ArrayList<>(avgEventTimings.keySet());
267             Collections.sort(eventNames);
268             for (String eventName : eventNames) {
269                 pw.printf("%s: %.2f\n", eventName, avgEventTimings.get(eventName));
270             }
271             pw.decreaseIndent();
272             pw.decreaseIndent();
273         }
274     }
275 
EventManager(@onNull SessionManager.ISessionIdQueryHandler l)276     public EventManager(@NonNull SessionManager.ISessionIdQueryHandler l) {
277         mSessionIdHandler = l;
278     }
279 
event(Loggable recordEntry, String event, Object data)280     public void event(Loggable recordEntry, String event, Object data) {
281         String currentSessionID = mSessionIdHandler.getSessionId();
282 
283         if (recordEntry == null) {
284             Log.i(TAG, "Non-call EVENT: %s, %s", event, data);
285             return;
286         }
287         synchronized (mEventRecords) {
288             if (!mCallEventRecordMap.containsKey(recordEntry)) {
289                 EventRecord newRecord = new EventRecord(recordEntry);
290                 addEventRecord(newRecord);
291             }
292 
293             EventRecord record = mCallEventRecordMap.get(recordEntry);
294             record.addEvent(event, currentSessionID, data);
295         }
296     }
297 
event(Loggable recordEntry, String event, String format, Object... args)298     public void event(Loggable recordEntry, String event, String format, Object... args) {
299         String msg;
300         try {
301             msg = (args == null || args.length == 0) ? format
302                     : String.format(Locale.US, format, args);
303         } catch (IllegalFormatException ife) {
304             Log.e(this, ife, "IllegalFormatException: formatString='%s' numArgs=%d", format,
305                     args.length);
306             msg = format + " (An error occurred while formatting the message.)";
307         }
308 
309         event(recordEntry, event, msg);
310     }
311 
dumpEvents(IndentingPrintWriter pw)312     public void dumpEvents(IndentingPrintWriter pw) {
313         pw.println("Historical Events:");
314         pw.increaseIndent();
315         for (EventRecord eventRecord : mEventRecords) {
316             eventRecord.dump(pw);
317         }
318         pw.decreaseIndent();
319     }
320 
321     /**
322      * Dumps events in a timeline format.
323      * @param pw The {@link IndentingPrintWriter} to output the timeline to.
324      * @hide
325      */
dumpEventsTimeline(IndentingPrintWriter pw)326     public void dumpEventsTimeline(IndentingPrintWriter pw) {
327         pw.println("Historical Events (sorted by time):");
328 
329         // Flatten event records out for sorting.
330         List<Pair<Loggable, Event>> events = new ArrayList<>();
331         for (EventRecord er : mEventRecords) {
332             for (Event ev : er.getEvents()) {
333                 events.add(new Pair<>(er.getRecordEntry(), ev));
334             }
335         }
336 
337         // Sort by event time. This might result in out-of-order seeming events if the timezone
338         // changes somewhere in the middle.
339         Comparator<Pair<Loggable, Event>> byEventTime =
340                 Comparator.comparingLong(e -> e.second.time);
341         events.sort(byEventTime);
342 
343         pw.increaseIndent();
344         for (Pair<Loggable, Event> event : events) {
345             pw.print(event.second.timestampString);
346             pw.print(",");
347             pw.print(event.first.getId());
348             pw.print(",");
349             pw.print(event.second.eventId);
350             pw.print(",");
351             pw.println(event.second.data);
352         }
353         pw.decreaseIndent();
354     }
355 
changeEventCacheSize(int newSize)356     public void changeEventCacheSize(int newSize) {
357 
358         // Resize the event queue.
359         LinkedBlockingQueue<EventRecord> oldEventLog = mEventRecords;
360         mEventRecords = new LinkedBlockingQueue<>(newSize);
361         mCallEventRecordMap.clear();
362 
363         oldEventLog.forEach((newRecord -> {
364             Loggable recordEntry = newRecord.getRecordEntry();
365             // Copy the existing queue into the new one.
366             // First remove the oldest entry if no new ones exist.
367             if (mEventRecords.remainingCapacity() == 0) {
368                 EventRecord record = mEventRecords.poll();
369                 if (record != null) {
370                     mCallEventRecordMap.remove(record.getRecordEntry());
371                 }
372             }
373 
374             // Now add a new entry
375             mEventRecords.add(newRecord);
376             mCallEventRecordMap.put(recordEntry, newRecord);
377 
378             // Don't worry about notifying mEventListeners, since we are just resizing the records.
379         }));
380     }
381 
registerEventListener(EventListener e)382     public void registerEventListener(EventListener e) {
383         if (e != null) {
384             synchronized (mSync) {
385                 mEventListeners.add(e);
386             }
387         }
388     }
389 
390     @VisibleForTesting
getEventRecords()391     public LinkedBlockingQueue<EventRecord> getEventRecords() {
392         return mEventRecords;
393     }
394 
395     @VisibleForTesting
getCallEventRecordMap()396     public Map<Loggable, EventRecord> getCallEventRecordMap() {
397         return mCallEventRecordMap;
398     }
399 
addEventRecord(EventRecord newRecord)400     private void addEventRecord(EventRecord newRecord) {
401         Loggable recordEntry = newRecord.getRecordEntry();
402 
403         // First remove the oldest entry if no new ones exist.
404         if (mEventRecords.remainingCapacity() == 0) {
405             EventRecord record = mEventRecords.poll();
406             if (record != null) {
407                 mCallEventRecordMap.remove(record.getRecordEntry());
408             }
409         }
410 
411         // Now add a new entry
412         mEventRecords.add(newRecord);
413         mCallEventRecordMap.put(recordEntry, newRecord);
414         synchronized (mSync) {
415             for (EventListener l : mEventListeners) {
416                 l.eventRecordAdded(newRecord);
417             }
418         }
419     }
420 }
421