• 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.Nullable;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.os.Process;
25 import android.provider.Settings;
26 import android.telecom.Log;
27 import android.util.Base64;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.server.telecom.flags.Flags;
31 
32 import java.nio.ByteBuffer;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.concurrent.ConcurrentHashMap;
38 
39 /**
40  * SessionManager manages the active sessions in a HashMap, which maps the active thread(s) to the
41  * associated {@link Session}s.
42  * <p>
43  * Note: Sessions assume that session structure modification is synchronized on this object - only
44  * one thread can modify the structure of any Session at one time. Printing the current session to
45  * the log is not synchronized because we should not clean up a session chain while printing from
46  * another Thread. Either the Session chain is still active and can not be cleaned up yet, or the
47  * Session chain has ended and we are cleaning up.
48  * @hide
49  */
50 public class SessionManager {
51 
52     // Currently using 3 letters, So don't exceed 64^3
53     private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144;
54     // This parameter can be overridden in Telecom's Timeouts class.
55     private static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds
56     private static final String LOGGING_TAG = "Logging";
57     private static final String TIMEOUTS_PREFIX = "telecom.";
58 
59     // Synchronized in all method calls
60     private int sCodeEntryCounter = 0;
61     private Context mContext;
62 
63     @VisibleForTesting
64     public final ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(64);
65     private final java.lang.Runnable mCleanStaleSessions;
66     private final Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper());
67 
68     // Overridden in LogTest to skip query to ContentProvider
69     private interface ISessionCleanupTimeoutMs {
get()70         long get();
71     }
72 
73     // Overridden in tests to provide test Thread IDs
74     public interface ICurrentThreadId {
get()75         int get();
76     }
77 
78     @VisibleForTesting
79     public ICurrentThreadId mCurrentThreadId = Process::myTid;
80 
81     private ISessionCleanupTimeoutMs mSessionCleanupTimeoutMs = () -> {
82         // mContext may be null in some cases, such as testing. For these cases, use the
83         // default value.
84         if (mContext == null) {
85             return DEFAULT_SESSION_TIMEOUT_MS;
86         }
87         return getCleanupTimeout(mContext);
88     };
89 
90     // Usage is synchronized on this class.
91     private final List<ISessionListener> mSessionListeners = new ArrayList<>();
92 
93     public interface ISessionListener {
94         /**
95          * This method is run when a full Session has completed.
96          * @param sessionName The name of the Session that has completed.
97          * @param timeMs The time it took to complete in ms.
98          */
sessionComplete(String sessionName, long timeMs)99         void sessionComplete(String sessionName, long timeMs);
100     }
101 
102     public interface ISessionIdQueryHandler {
getSessionId()103         String getSessionId();
104     }
105 
setContext(Context context)106     public void setContext(Context context) {
107         mContext = context;
108     }
109 
SessionManager()110     public SessionManager() {
111         mCleanStaleSessions = () -> cleanupStaleSessions(getSessionCleanupTimeoutMs());
112     }
113 
114     @VisibleForTesting
SessionManager(java.lang.Runnable cleanStaleSessionsRunnable)115     public SessionManager(java.lang.Runnable cleanStaleSessionsRunnable) {
116         mCleanStaleSessions = cleanStaleSessionsRunnable;
117     }
118 
getSessionCleanupTimeoutMs()119     private long getSessionCleanupTimeoutMs() {
120         return mSessionCleanupTimeoutMs.get();
121     }
122 
resetStaleSessionTimer()123     private void resetStaleSessionTimer() {
124         if (!Flags.endSessionImprovements()) {
125             resetStaleSessionTimerOld();
126             return;
127         }
128         // Will be null in Log Testing
129         if (mCleanStaleSessions == null) return;
130         synchronized (mSessionCleanupHandler) {
131             if (!mSessionCleanupHandler.hasCallbacks(mCleanStaleSessions)) {
132                 mSessionCleanupHandler.postDelayed(mCleanStaleSessions,
133                         getSessionCleanupTimeoutMs());
134             }
135         }
136     }
137 
resetStaleSessionTimerOld()138     private synchronized void resetStaleSessionTimerOld() {
139         if (mCleanStaleSessions == null) return;
140         mSessionCleanupHandler.removeCallbacksAndMessages(null);
141         mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs());
142     }
143 
144     /**
145      * Determines whether or not to start a new session or continue an existing session based on
146      * the {@link Session.Info} info passed into startSession. If info is null, a new Session is
147      * created. This code must be accompanied by endSession() at the end of the Session.
148      */
startSession(Session.Info info, String shortMethodName, String callerIdentification)149     public synchronized void startSession(Session.Info info, String shortMethodName,
150             String callerIdentification) {
151         // Start a new session normally if the
152         if(info == null) {
153             startSession(shortMethodName, callerIdentification);
154         } else {
155             startExternalSession(info, shortMethodName);
156         }
157     }
158 
159     /**
160      * Call at an entry point to the Telecom code to track the session. This code must be
161      * accompanied by a Log.endSession().
162      */
startSession(String shortMethodName, String callerIdentification)163     public synchronized void startSession(String shortMethodName,
164             String callerIdentification) {
165         resetStaleSessionTimer();
166         int threadId = getCallingThreadId();
167         Session activeSession = mSessionMapper.get(threadId);
168         // We have called startSession within an active session that has not ended... Register this
169         // session as a subsession.
170         if (activeSession != null) {
171             Session childSession = createSubsession(true);
172             continueSession(childSession, shortMethodName);
173             return;
174         }
175         Session newSession = new Session(getNextSessionID(), shortMethodName,
176                 System.currentTimeMillis(), false, false, callerIdentification);
177         mSessionMapper.put(threadId, newSession);
178         Log.d(LOGGING_TAG, Session.START_SESSION);
179     }
180 
181     /**
182      * Registers an external Session with the Manager using that external Session's sessionInfo.
183      * Log.endSession will still need to be called at the end of the session.
184      * @param sessionInfo Describes the external Session's information.
185      * @param shortMethodName The method name of the new session that is being started.
186      */
startExternalSession(Session.Info sessionInfo, String shortMethodName)187     public synchronized void startExternalSession(Session.Info sessionInfo,
188             String shortMethodName) {
189         if(sessionInfo == null) {
190             return;
191         }
192 
193         int threadId = getCallingThreadId();
194         Session threadSession = mSessionMapper.get(threadId);
195         if (threadSession != null) {
196             // We should never get into a situation where there is already an active session AND
197             // an external session is added. We are just using that active session.
198             Log.w(LOGGING_TAG, "trying to start an external session with a session " +
199                     "already active.");
200             return;
201         }
202 
203         // Create Session from Info and add to the sessionMapper under this ID.
204         Session externalSession = new Session(Session.EXTERNAL_INDICATOR + sessionInfo.sessionId,
205                 sessionInfo.methodPath, System.currentTimeMillis(), false, true,
206                 sessionInfo.ownerInfo);
207         // Mark the external session as already completed, since we have no way of knowing when
208         // the external session actually has completed.
209         externalSession.markSessionCompleted(Session.UNDEFINED);
210         // Track the external session with the SessionMapper so that we can create and continue
211         // an active subsession based on it.
212         mSessionMapper.put(threadId, externalSession);
213         Log.d(LOGGING_TAG, Session.START_EXTERNAL_SESSION);
214         // Create a subsession from this external Session parent node
215         Session childSession = createSubsession();
216         continueSession(childSession, shortMethodName);
217     }
218 
219     /**
220      * Notifies the logging system that a subsession will be run at a later point and
221      * allocates the resources. Returns a session object that must be used in
222      * Log.continueSession(...) to start the subsession.
223      */
createSubsession()224     public Session createSubsession() {
225         return createSubsession(false);
226     }
227 
228     /**
229      * Creates a new subsession based on an existing session. Will not be started until
230      * {@link #continueSession(Session, String)} or {@link #cancelSubsession(Session)} is called.
231      * <p>
232      * Only public for testing!
233      * @param isStartedFromActiveSession true if this subsession is being created for a task on the
234      *     same thread, false if it is being created for a related task on another thread.
235      * @return a new {@link Session}, call {@link #continueSession(Session, String)} to continue the
236      * session and {@link #endSession()} when done with this subsession.
237      */
238     @VisibleForTesting
createSubsession(boolean isStartedFromActiveSession)239     public synchronized Session createSubsession(boolean isStartedFromActiveSession) {
240         int threadId = getCallingThreadId();
241         Session threadSession = mSessionMapper.get(threadId);
242         if (threadSession == null) {
243             Log.d(LOGGING_TAG, "Log.createSubsession was called with no session " +
244                     "active.");
245             return null;
246         }
247         // Start execution time of the session will be overwritten in continueSession(...).
248         Session newSubsession = new Session(threadSession.getNextChildId(),
249                 threadSession.getShortMethodName(), System.currentTimeMillis(),
250                 isStartedFromActiveSession, false, threadSession.getOwnerInfo());
251         threadSession.addChild(newSubsession);
252         newSubsession.setParentSession(threadSession);
253 
254         if (!isStartedFromActiveSession) {
255             Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION);
256         } else {
257             Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION +
258                     " (Invisible subsession)");
259         }
260         return newSubsession;
261     }
262 
getExternalSession()263     public synchronized Session.Info getExternalSession() {
264         return getExternalSession(null /* ownerInfo */);
265     }
266 
267     /**
268      * Retrieve the information of the currently active Session. This information is parcelable and
269      * is used to create an external Session ({@link #startExternalSession(Session.Info, String)}).
270      * If there is no Session active, this method will return null.
271      * @param ownerInfo Owner information for the session.
272      * @return The session information
273      */
getExternalSession(@ullable String ownerInfo)274     public synchronized Session.Info getExternalSession(@Nullable String ownerInfo) {
275         int threadId = getCallingThreadId();
276         Session threadSession = mSessionMapper.get(threadId);
277         if (threadSession == null) {
278             Log.d(LOGGING_TAG, "Log.getExternalSession was called with no session " +
279                     "active.");
280             return null;
281         }
282         return threadSession.getExternalInfo(ownerInfo);
283     }
284 
285     /**
286      * Cancels a subsession that had Log.createSubsession() called on it, but will never have
287      * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned
288      * gracefully instead of being removed by the mSessionCleanupHandler forcefully later.
289      */
cancelSubsession(Session subsession)290     public synchronized void cancelSubsession(Session subsession) {
291         if (subsession == null) {
292             return;
293         }
294 
295         subsession.markSessionCompleted(Session.UNDEFINED);
296         cleanupSessionTreeAndNotify(subsession);
297     }
298 
299     /**
300      * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method
301      * must be called at the end of this method. The full session will complete when all
302      * subsessions are completed.
303      */
continueSession(Session subsession, String shortMethodName)304     public synchronized void continueSession(Session subsession, String shortMethodName) {
305         if (subsession == null) {
306             return;
307         }
308         resetStaleSessionTimer();
309         subsession.setShortMethodName(shortMethodName);
310         subsession.setExecutionStartTimeMs(System.currentTimeMillis());
311         Session parentSession = subsession.getParentSession();
312         if (parentSession == null) {
313             Log.i(LOGGING_TAG, "Log.continueSession was called with no session " +
314                     "active for method " + shortMethodName);
315             return;
316         }
317 
318         mSessionMapper.put(getCallingThreadId(), subsession);
319         if (!subsession.isStartedFromActiveSession()) {
320             Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
321         } else {
322             Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION +
323                     " (Invisible Subsession) with Method " + shortMethodName);
324         }
325     }
326 
327     /**
328      * Ends the current session/subsession. Must be called after a Log.startSession(...) and
329      * Log.continueSession(...) call.
330      */
endSession()331     public synchronized void endSession() {
332         int threadId = getCallingThreadId();
333         Session completedSession = mSessionMapper.get(threadId);
334         if (completedSession == null) {
335             Log.w(LOGGING_TAG, "Log.endSession was called with no session active.");
336             return;
337         }
338 
339         completedSession.markSessionCompleted(System.currentTimeMillis());
340         if (!completedSession.isStartedFromActiveSession()) {
341             Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
342                     completedSession.getLocalExecutionTime() + " mS)");
343         } else {
344             Log.v(LOGGING_TAG, Session.END_SUBSESSION +
345                     " (Invisible Subsession) (dur: " + completedSession.getLocalExecutionTime() +
346                     " ms)");
347         }
348         // Remove after completed so that reference still exists for logging the end events
349         Session parentSession = completedSession.getParentSession();
350         mSessionMapper.remove(threadId);
351         cleanupSessionTreeAndNotify(completedSession);
352         // If this subsession was started from a parent session using Log.startSession, return the
353         // ThreadID back to the parent after completion.
354         if (parentSession != null && !parentSession.isSessionCompleted() &&
355                 completedSession.isStartedFromActiveSession()) {
356             mSessionMapper.put(threadId, parentSession);
357         }
358     }
359 
360     /**
361      * Move up the session tree and remove completed sessions until we either hit a session that was
362      * not completed yet or we reach the root node. Once we reach the root node, we will report the
363      * session times to session complete listeners.
364      * @param session The Session to clean up.
365      */
cleanupSessionTreeAndNotify(Session session)366     private void cleanupSessionTreeAndNotify(Session session) {
367         if (session == null) return;
368         if (!Flags.endSessionImprovements()) {
369             endParentSessionsRecursive(session);
370             return;
371         }
372         Session currSession = session;
373         // Traverse upwards and unlink until we either hit the root node or a node that isn't
374         // complete yet.
375         while (currSession != null) {
376             if (!currSession.isSessionCompleted() || !currSession.getChildSessions().isEmpty()) {
377                 // We will return once the active session is completed.
378                 return;
379             }
380             Session parentSession = currSession.getParentSession();
381             currSession.setParentSession(null);
382             // The session is either complete when we have reached the top node or we have reached
383             // the node where the parent is external. We only want to report the time it took to
384             // complete the local session, so for external nodes, report finished when the sub-node
385             // completes.
386             boolean reportSessionComplete =
387                     (parentSession == null && !currSession.isExternal())
388                             || (parentSession != null && parentSession.isExternal());
389             if (parentSession != null) parentSession.removeChild(currSession);
390             if (reportSessionComplete) {
391                 long fullSessionTimeMs = System.currentTimeMillis()
392                         - currSession.getExecutionStartTimeMilliseconds();
393                 Log.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs
394                         + " ms): " + currSession);
395                 notifySessionCompleteListeners(currSession.getShortMethodName(), fullSessionTimeMs);
396             }
397             currSession = parentSession;
398         }
399     }
400 
401     // Recursively deletes all complete parent sessions of the current subsession if it is a leaf.
endParentSessionsRecursive(Session subsession)402     private void endParentSessionsRecursive(Session subsession) {
403         // Session is not completed or not currently a leaf, so we can not remove because a child is
404         // still running
405         if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) {
406             return;
407         }
408         Session parentSession = subsession.getParentSession();
409         if (parentSession != null) {
410             subsession.setParentSession(null);
411             parentSession.removeChild(subsession);
412             // Report the child session of the external session as being complete to the listeners,
413             // not the external session itself.
414             if (parentSession.isExternal()) {
415                 long fullSessionTimeMs =
416                         System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
417                 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
418             }
419             endParentSessionsRecursive(parentSession);
420         } else {
421             // All of the subsessions have been completed and it is time to report on the full
422             // running time of the session.
423             long fullSessionTimeMs =
424                     System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
425             Log.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs
426                     + " ms): " + subsession.toString());
427             if (!subsession.isExternal()) {
428                 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
429             }
430         }
431     }
432 
notifySessionCompleteListeners(String methodName, long sessionTimeMs)433     private void notifySessionCompleteListeners(String methodName, long sessionTimeMs) {
434         synchronized (mSessionListeners) {
435             for (ISessionListener l : mSessionListeners) {
436                 l.sessionComplete(methodName, sessionTimeMs);
437             }
438         }
439     }
440 
getSessionId()441     public String getSessionId() {
442         Session currentSession = mSessionMapper.get(getCallingThreadId());
443         return currentSession != null ? currentSession.toString() : "";
444     }
445 
registerSessionListener(ISessionListener l)446     public void registerSessionListener(ISessionListener l) {
447         synchronized (mSessionListeners) {
448             mSessionListeners.add(l);
449         }
450     }
451 
getNextSessionID()452     private synchronized String getNextSessionID() {
453         Integer nextId = sCodeEntryCounter++;
454         if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) {
455             restartSessionCounter();
456             nextId = sCodeEntryCounter++;
457         }
458         return getBase64Encoding(nextId);
459     }
460 
restartSessionCounter()461     private synchronized void restartSessionCounter() {
462         sCodeEntryCounter = 0;
463     }
464 
getBase64Encoding(int number)465     private String getBase64Encoding(int number) {
466         byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array();
467         idByteArray = Arrays.copyOfRange(idByteArray, 2, 4);
468         return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING);
469     }
470 
getCallingThreadId()471     private int getCallingThreadId() {
472         return mCurrentThreadId.get();
473     }
474 
475     /**
476      * @return A String representation of the active sessions at the time that this method is
477      * called.
478      */
479     @VisibleForTesting
printActiveSessions()480     public synchronized String printActiveSessions() {
481         StringBuilder message = new StringBuilder();
482         for (ConcurrentHashMap.Entry<Integer, Session> entry : mSessionMapper.entrySet()) {
483             message.append(entry.getValue().printFullSessionTree());
484             message.append("\n");
485         }
486         return message.toString();
487     }
488 
489     @VisibleForTesting
cleanupStaleSessions(long timeoutMs)490     public synchronized void cleanupStaleSessions(long timeoutMs) {
491         StringBuilder logMessage = new StringBuilder("Stale Sessions Cleaned:");
492         boolean isSessionsStale = false;
493         long currentTimeMs = System.currentTimeMillis();
494         // Remove references that are in the Session Mapper (causing GC to occur) on
495         // sessions that are lasting longer than DEFAULT_SESSION_TIMEOUT_MS.
496         // If this occurs, then there is most likely a Session active that never had
497         // Log.endSession called on it.
498         for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it =
499              mSessionMapper.entrySet().iterator(); it.hasNext(); ) {
500             ConcurrentHashMap.Entry<Integer, Session> entry = it.next();
501             Session session = entry.getValue();
502             long runTime = currentTimeMs - session.getExecutionStartTimeMilliseconds();
503             if (runTime > timeoutMs) {
504                 it.remove();
505                 logMessage.append("\n");
506                 logMessage.append("[");
507                 logMessage.append(runTime);
508                 logMessage.append("ms] ");
509                 logMessage.append(session.printFullSessionTree());
510                 isSessionsStale = true;
511             }
512         }
513         if (isSessionsStale) {
514             Log.w(LOGGING_TAG, logMessage.toString());
515         } else {
516             Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
517         }
518     }
519 
520     /**
521      * Returns the amount of time after a Logging session has been started that Telecom is set to
522      * perform a sweep to check and make sure that the session is still not incomplete (stale).
523      */
getCleanupTimeout(Context context)524     private long getCleanupTimeout(Context context) {
525         final ContentResolver cr = context.getContentResolver();
526         return Settings.Secure.getLongForUser(cr, TIMEOUTS_PREFIX
527                         + "stale_session_cleanup_timeout_millis", DEFAULT_SESSION_TIMEOUT_MS,
528                 cr.getUserId());
529     }
530 }
531