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