Home | History | Annotate | Download | only in Logging
      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          */
     62         String getId();
     63 
     64         /**
     65          * @return Formatted information about the state that will be printed out later in the logs.
     66          */
     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          */
     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 
    105         public TimedEventPair(String request, String response, String name) {
    106             this.mRequest = request;
    107             this.mResponse = response;
    108             this.mName = name;
    109         }
    110 
    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 
    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 
    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 
    154             public EventTiming(String name, long time) {
    155                 this.name = name;
    156                 this.time = time;
    157             }
    158 
    159             public String getKey() {
    160                 return name;
    161             }
    162 
    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 
    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 
    186         public EventRecord(Loggable recordEntry) {
    187             mRecordEntry = recordEntry;
    188         }
    189 
    190         public Loggable getRecordEntry() {
    191             return mRecordEntry;
    192         }
    193 
    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 
    199         public List<Event> getEvents() {
    200             return new LinkedList<>(mEvents);
    201         }
    202 
    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 
    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 
    280     public EventManager(@NonNull SessionManager.ISessionIdQueryHandler l) {
    281         mSessionIdHandler = l;
    282     }
    283 
    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 
    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 
    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      */
    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 
    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 
    386     public void registerEventListener(EventListener e) {
    387         if (e != null) {
    388             synchronized (mSync) {
    389                 mEventListeners.add(e);
    390             }
    391         }
    392     }
    393 
    394     @VisibleForTesting
    395     public LinkedBlockingQueue<EventRecord> getEventRecords() {
    396         return mEventRecords;
    397     }
    398 
    399     @VisibleForTesting
    400     public Map<Loggable, EventRecord> getCallEventRecordMap() {
    401         return mCallEventRecordMap;
    402     }
    403 
    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