blob: 173e7ee5538786c44fdb1669859da931b4d0a3f2 [file] [log] [blame]
Brad Ebinger51b98342016-09-22 16:30:46 -07001/*
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
17package android.telecom.Logging;
18
19import android.content.Context;
20import android.os.Handler;
21import android.os.Looper;
Brad Ebinger096d2822016-10-13 15:26:58 -070022import android.os.Process;
Brad Ebinger51b98342016-09-22 16:30:46 -070023import android.provider.Settings;
Brad Ebingera0dc9762016-10-21 09:41:29 -070024import android.telecom.Log;
Brad Ebinger51b98342016-09-22 16:30:46 -070025import android.util.Base64;
26
27import com.android.internal.annotations.VisibleForTesting;
28
29import java.nio.ByteBuffer;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Iterator;
33import java.util.List;
34import java.util.concurrent.ConcurrentHashMap;
35
36/**
37 * TODO: Create better Sessions Documentation
38 * @hide
39 */
40
41public class SessionManager {
42
43 // Currently using 3 letters, So don't exceed 64^3
44 private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144;
Brad Ebinger51b98342016-09-22 16:30:46 -070045 // This parameter can be overridden in Telecom's Timeouts class.
Brad Ebingera0dc9762016-10-21 09:41:29 -070046 private static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds
47 private static final String LOGGING_TAG = "Logging";
Brad Ebinger51b98342016-09-22 16:30:46 -070048 private static final String TIMEOUTS_PREFIX = "telecom.";
49
50 // Synchronized in all method calls
51 private int sCodeEntryCounter = 0;
52 private Context mContext;
53
54 @VisibleForTesting
Brad Ebinger096d2822016-10-13 15:26:58 -070055 public ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(100);
Brad Ebinger51b98342016-09-22 16:30:46 -070056 @VisibleForTesting
Brad Ebinger096d2822016-10-13 15:26:58 -070057 public java.lang.Runnable mCleanStaleSessions = () ->
Brad Ebinger51b98342016-09-22 16:30:46 -070058 cleanupStaleSessions(getSessionCleanupTimeoutMs());
Brad Ebingera0dc9762016-10-21 09:41:29 -070059 private Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper());
Brad Ebinger51b98342016-09-22 16:30:46 -070060
61 // Overridden in LogTest to skip query to ContentProvider
62 private interface ISessionCleanupTimeoutMs {
63 long get();
64 }
65
Brad Ebinger096d2822016-10-13 15:26:58 -070066 // Overridden in tests to provide test Thread IDs
67 public interface ICurrentThreadId {
68 int get();
69 }
70
Brad Ebinger51b98342016-09-22 16:30:46 -070071 @VisibleForTesting
Brad Ebinger096d2822016-10-13 15:26:58 -070072 public ICurrentThreadId mCurrentThreadId = Process::myTid;
73
Brad Ebingera0dc9762016-10-21 09:41:29 -070074 private ISessionCleanupTimeoutMs mSessionCleanupTimeoutMs = () -> {
Brad Ebinger51b98342016-09-22 16:30:46 -070075 // mContext may be null in some cases, such as testing. For these cases, use the
76 // default value.
77 if (mContext == null) {
78 return DEFAULT_SESSION_TIMEOUT_MS;
79 }
80 return getCleanupTimeout(mContext);
81 };
82
83 // Usage is synchronized on this class.
84 private List<ISessionListener> mSessionListeners = new ArrayList<>();
85
86 public interface ISessionListener {
87 /**
88 * This method is run when a full Session has completed.
89 * @param sessionName The name of the Session that has completed.
90 * @param timeMs The time it took to complete in ms.
91 */
92 void sessionComplete(String sessionName, long timeMs);
93 }
94
95 public interface ISessionIdQueryHandler {
96 String getSessionId();
97 }
98
99 public void setContext(Context context) {
100 mContext = context;
101 }
102
103 public SessionManager() {
104 }
105
106 private long getSessionCleanupTimeoutMs() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700107 return mSessionCleanupTimeoutMs.get();
Brad Ebinger51b98342016-09-22 16:30:46 -0700108 }
109
110 private synchronized void resetStaleSessionTimer() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700111 mSessionCleanupHandler.removeCallbacksAndMessages(null);
Brad Ebinger51b98342016-09-22 16:30:46 -0700112 // Will be null in Log Testing
Brad Ebinger096d2822016-10-13 15:26:58 -0700113 if (mCleanStaleSessions != null) {
114 mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs());
Brad Ebinger51b98342016-09-22 16:30:46 -0700115 }
116 }
117
118 /**
Brad Ebingera0dc9762016-10-21 09:41:29 -0700119 * Determines whether or not to start a new session or continue an existing session based on
120 * the {@link Session.Info} info passed into startSession. If info is null, a new Session is
121 * created. This code must be accompanied by endSession() at the end of the Session.
122 */
123 public synchronized void startSession(Session.Info info, String shortMethodName,
124 String callerIdentification) {
125 // Start a new session normally if the
126 if(info == null) {
127 startSession(shortMethodName, callerIdentification);
128 } else {
129 startExternalSession(info, shortMethodName);
130 }
131 }
132
133 /**
Brad Ebinger51b98342016-09-22 16:30:46 -0700134 * Call at an entry point to the Telecom code to track the session. This code must be
135 * accompanied by a Log.endSession().
136 */
137 public synchronized void startSession(String shortMethodName,
138 String callerIdentification) {
139 resetStaleSessionTimer();
140 int threadId = getCallingThreadId();
Brad Ebinger096d2822016-10-13 15:26:58 -0700141 Session activeSession = mSessionMapper.get(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700142 // We have called startSession within an active session that has not ended... Register this
143 // session as a subsession.
144 if (activeSession != null) {
145 Session childSession = createSubsession(true);
146 continueSession(childSession, shortMethodName);
147 return;
Brad Ebingera0dc9762016-10-21 09:41:29 -0700148 } else {
149 // Only Log that we are starting the parent session.
150 Log.d(LOGGING_TAG, Session.START_SESSION);
Brad Ebinger51b98342016-09-22 16:30:46 -0700151 }
152 Session newSession = new Session(getNextSessionID(), shortMethodName,
Brad Ebingera0dc9762016-10-21 09:41:29 -0700153 System.currentTimeMillis(), false, callerIdentification);
Brad Ebinger096d2822016-10-13 15:26:58 -0700154 mSessionMapper.put(threadId, newSession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700155 }
156
Brad Ebingera0dc9762016-10-21 09:41:29 -0700157 /**
158 * Registers an external Session with the Manager using that external Session's sessionInfo.
159 * Log.endSession will still need to be called at the end of the session.
160 * @param sessionInfo Describes the external Session's information.
161 * @param shortMethodName The method name of the new session that is being started.
162 */
163 public synchronized void startExternalSession(Session.Info sessionInfo,
164 String shortMethodName) {
165 if(sessionInfo == null) {
166 return;
167 }
168
169 int threadId = getCallingThreadId();
170 Session threadSession = mSessionMapper.get(threadId);
171 if (threadSession != null) {
172 // We should never get into a situation where there is already an active session AND
173 // an external session is added. We are just using that active session.
174 Log.w(LOGGING_TAG, "trying to start an external session with a session " +
175 "already active.");
176 return;
177 }
178
179 // Create Session from Info and add to the sessionMapper under this ID.
180 Session externalSession = new Session(Session.EXTERNAL_INDICATOR + sessionInfo.sessionId,
181 sessionInfo.shortMethodName, System.currentTimeMillis(),
182 false /*isStartedFromActiveSession*/, null);
183 externalSession.setIsExternal(true);
184 // Mark the external session as already completed, since we have no way of knowing when
185 // the external session actually has completed.
186 externalSession.markSessionCompleted(Session.UNDEFINED);
187 // Track the external session with the SessionMapper so that we can create and continue
188 // an active subsession based on it.
189 mSessionMapper.put(threadId, externalSession);
190 // Create a subsession from this external Session parent node
191 Session childSession = createSubsession();
192 continueSession(childSession, shortMethodName);
193
194 Log.d(LOGGING_TAG, Session.START_SESSION);
195 }
Brad Ebinger51b98342016-09-22 16:30:46 -0700196
197 /**
198 * Notifies the logging system that a subsession will be run at a later point and
199 * allocates the resources. Returns a session object that must be used in
200 * Log.continueSession(...) to start the subsession.
201 */
202 public Session createSubsession() {
203 return createSubsession(false);
204 }
205
206 private synchronized Session createSubsession(boolean isStartedFromActiveSession) {
207 int threadId = getCallingThreadId();
Brad Ebinger096d2822016-10-13 15:26:58 -0700208 Session threadSession = mSessionMapper.get(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700209 if (threadSession == null) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700210 Log.d(LOGGING_TAG, "Log.createSubsession was called with no session " +
Brad Ebinger51b98342016-09-22 16:30:46 -0700211 "active.");
212 return null;
213 }
214 // Start execution time of the session will be overwritten in continueSession(...).
215 Session newSubsession = new Session(threadSession.getNextChildId(),
Brad Ebingera0dc9762016-10-21 09:41:29 -0700216 threadSession.getShortMethodName(), System.currentTimeMillis(),
Brad Ebinger51b98342016-09-22 16:30:46 -0700217 isStartedFromActiveSession, null);
218 threadSession.addChild(newSubsession);
219 newSubsession.setParentSession(threadSession);
220
221 if (!isStartedFromActiveSession) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700222 Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " +
Brad Ebinger51b98342016-09-22 16:30:46 -0700223 newSubsession.toString());
224 } else {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700225 Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700226 " (Invisible subsession)");
227 }
228 return newSubsession;
229 }
230
231 /**
232 * Cancels a subsession that had Log.createSubsession() called on it, but will never have
233 * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned
Brad Ebinger096d2822016-10-13 15:26:58 -0700234 * gracefully instead of being removed by the mSessionCleanupHandler forcefully later.
Brad Ebinger51b98342016-09-22 16:30:46 -0700235 */
236 public synchronized void cancelSubsession(Session subsession) {
237 if (subsession == null) {
238 return;
239 }
240
Brad Ebinger096d2822016-10-13 15:26:58 -0700241 subsession.markSessionCompleted(Session.UNDEFINED);
Brad Ebinger51b98342016-09-22 16:30:46 -0700242 endParentSessions(subsession);
243 }
244
245 /**
246 * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method
247 * must be called at the end of this method. The full session will complete when all
248 * subsessions are completed.
249 */
250 public synchronized void continueSession(Session subsession, String shortMethodName) {
251 if (subsession == null) {
252 return;
253 }
254 resetStaleSessionTimer();
Brad Ebingera0dc9762016-10-21 09:41:29 -0700255 subsession.setShortMethodName(shortMethodName);
Brad Ebinger51b98342016-09-22 16:30:46 -0700256 subsession.setExecutionStartTimeMs(System.currentTimeMillis());
257 Session parentSession = subsession.getParentSession();
258 if (parentSession == null) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700259 Log.i(LOGGING_TAG, "Log.continueSession was called with no session " +
Brad Ebinger096d2822016-10-13 15:26:58 -0700260 "active for method " + shortMethodName);
Brad Ebinger51b98342016-09-22 16:30:46 -0700261 return;
262 }
263
Brad Ebinger096d2822016-10-13 15:26:58 -0700264 mSessionMapper.put(getCallingThreadId(), subsession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700265 if (!subsession.isStartedFromActiveSession()) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700266 Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
Brad Ebinger51b98342016-09-22 16:30:46 -0700267 } else {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700268 Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700269 " (Invisible Subsession) with Method " + shortMethodName);
270 }
271 }
272
273 /**
274 * Ends the current session/subsession. Must be called after a Log.startSession(...) and
275 * Log.continueSession(...) call.
276 */
277 public synchronized void endSession() {
278 int threadId = getCallingThreadId();
Brad Ebinger096d2822016-10-13 15:26:58 -0700279 Session completedSession = mSessionMapper.get(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700280 if (completedSession == null) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700281 Log.w(LOGGING_TAG, "Log.endSession was called with no session active.");
Brad Ebinger51b98342016-09-22 16:30:46 -0700282 return;
283 }
284
285 completedSession.markSessionCompleted(System.currentTimeMillis());
286 if (!completedSession.isStartedFromActiveSession()) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700287 Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
Brad Ebinger51b98342016-09-22 16:30:46 -0700288 completedSession.getLocalExecutionTime() + " mS)");
289 } else {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700290 Log.v(LOGGING_TAG, Session.END_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700291 " (Invisible Subsession) (dur: " + completedSession.getLocalExecutionTime() +
292 " ms)");
293 }
294 // Remove after completed so that reference still exists for logging the end events
295 Session parentSession = completedSession.getParentSession();
Brad Ebinger096d2822016-10-13 15:26:58 -0700296 mSessionMapper.remove(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700297 endParentSessions(completedSession);
298 // If this subsession was started from a parent session using Log.startSession, return the
299 // ThreadID back to the parent after completion.
300 if (parentSession != null && !parentSession.isSessionCompleted() &&
301 completedSession.isStartedFromActiveSession()) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700302 mSessionMapper.put(threadId, parentSession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700303 }
304 }
305
306 // Recursively deletes all complete parent sessions of the current subsession if it is a leaf.
307 private void endParentSessions(Session subsession) {
308 // Session is not completed or not currently a leaf, so we can not remove because a child is
309 // still running
310 if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) {
311 return;
312 }
Brad Ebinger51b98342016-09-22 16:30:46 -0700313 Session parentSession = subsession.getParentSession();
314 if (parentSession != null) {
315 subsession.setParentSession(null);
316 parentSession.removeChild(subsession);
Brad Ebingera0dc9762016-10-21 09:41:29 -0700317 // Report the child session of the external session as being complete to the listeners,
318 // not the external session itself.
319 if (parentSession.isExternal()) {
320 long fullSessionTimeMs =
321 System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
322 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
323 }
Brad Ebinger51b98342016-09-22 16:30:46 -0700324 endParentSessions(parentSession);
325 } else {
326 // All of the subsessions have been completed and it is time to report on the full
327 // running time of the session.
328 long fullSessionTimeMs =
329 System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
Brad Ebingera0dc9762016-10-21 09:41:29 -0700330 Log.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs
Brad Ebinger51b98342016-09-22 16:30:46 -0700331 + " ms): " + subsession.toString());
Brad Ebingera0dc9762016-10-21 09:41:29 -0700332 if (!subsession.isExternal()) {
333 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
Brad Ebinger51b98342016-09-22 16:30:46 -0700334 }
335 }
336 }
337
Brad Ebingera0dc9762016-10-21 09:41:29 -0700338 private void notifySessionCompleteListeners(String methodName, long sessionTimeMs) {
339 for (ISessionListener l : mSessionListeners) {
340 l.sessionComplete(methodName, sessionTimeMs);
341 }
342 }
343
Brad Ebinger51b98342016-09-22 16:30:46 -0700344 public String getSessionId() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700345 Session currentSession = mSessionMapper.get(getCallingThreadId());
Brad Ebinger51b98342016-09-22 16:30:46 -0700346 return currentSession != null ? currentSession.toString() : "";
347 }
348
349 public synchronized void registerSessionListener(ISessionListener l) {
350 if (l != null) {
351 mSessionListeners.add(l);
352 }
353 }
354
355 private synchronized String getNextSessionID() {
356 Integer nextId = sCodeEntryCounter++;
357 if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) {
358 restartSessionCounter();
359 nextId = sCodeEntryCounter++;
360 }
361 return getBase64Encoding(nextId);
362 }
363
Brad Ebingera0dc9762016-10-21 09:41:29 -0700364 private synchronized void restartSessionCounter() {
Brad Ebinger51b98342016-09-22 16:30:46 -0700365 sCodeEntryCounter = 0;
366 }
367
Brad Ebingera0dc9762016-10-21 09:41:29 -0700368 private String getBase64Encoding(int number) {
Brad Ebinger51b98342016-09-22 16:30:46 -0700369 byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array();
370 idByteArray = Arrays.copyOfRange(idByteArray, 2, 4);
371 return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING);
372 }
373
Brad Ebingera0dc9762016-10-21 09:41:29 -0700374 private int getCallingThreadId() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700375 return mCurrentThreadId.get();
Brad Ebinger51b98342016-09-22 16:30:46 -0700376 }
377
378 @VisibleForTesting
Brad Ebingera0dc9762016-10-21 09:41:29 -0700379 public synchronized void cleanupStaleSessions(long timeoutMs) {
Brad Ebinger51b98342016-09-22 16:30:46 -0700380 String logMessage = "Stale Sessions Cleaned:\n";
381 boolean isSessionsStale = false;
382 long currentTimeMs = System.currentTimeMillis();
383 // Remove references that are in the Session Mapper (causing GC to occur) on
384 // sessions that are lasting longer than LOGGING_SESSION_TIMEOUT_MS.
385 // If this occurs, then there is most likely a Session active that never had
386 // Log.endSession called on it.
387 for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it =
Brad Ebinger096d2822016-10-13 15:26:58 -0700388 mSessionMapper.entrySet().iterator(); it.hasNext(); ) {
Brad Ebinger51b98342016-09-22 16:30:46 -0700389 ConcurrentHashMap.Entry<Integer, Session> entry = it.next();
390 Session session = entry.getValue();
391 if (currentTimeMs - session.getExecutionStartTimeMilliseconds() > timeoutMs) {
392 it.remove();
393 logMessage += session.printFullSessionTree() + "\n";
394 isSessionsStale = true;
395 }
396 }
397 if (isSessionsStale) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700398 Log.w(LOGGING_TAG, logMessage);
Brad Ebinger51b98342016-09-22 16:30:46 -0700399 } else {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700400 Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
Brad Ebinger51b98342016-09-22 16:30:46 -0700401 }
402 }
403
404 /**
405 * Returns the amount of time after a Logging session has been started that Telecom is set to
406 * perform a sweep to check and make sure that the session is still not incomplete (stale).
407 */
408 private long getCleanupTimeout(Context context) {
409 return Settings.Secure.getLong(context.getContentResolver(), TIMEOUTS_PREFIX +
410 "stale_session_cleanup_timeout_millis", DEFAULT_SESSION_TIMEOUT_MS);
411 }
412}