blob: 8ced7f8181c5d35f46090ea9dfd1a8545ec75794 [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 /**
Brad Ebinger3445f822016-10-24 16:40:49 -0700232 * Retrieve the information of the currently active Session. This information is parcelable and
233 * is used to create an external Session ({@link #startExternalSession(Session.Info, String)}).
234 * If there is no Session active, this method will return null.
235 */
236 public synchronized Session.Info getExternalSession() {
237 int threadId = getCallingThreadId();
238 Session threadSession = mSessionMapper.get(threadId);
239 if (threadSession == null) {
240 Log.d(LOGGING_TAG, "Log.getExternalSession was called with no session " +
241 "active.");
242 return null;
243 }
244
245 return threadSession.getInfo();
246 }
247
248 /**
Brad Ebinger51b98342016-09-22 16:30:46 -0700249 * Cancels a subsession that had Log.createSubsession() called on it, but will never have
250 * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned
Brad Ebinger096d2822016-10-13 15:26:58 -0700251 * gracefully instead of being removed by the mSessionCleanupHandler forcefully later.
Brad Ebinger51b98342016-09-22 16:30:46 -0700252 */
253 public synchronized void cancelSubsession(Session subsession) {
254 if (subsession == null) {
255 return;
256 }
257
Brad Ebinger096d2822016-10-13 15:26:58 -0700258 subsession.markSessionCompleted(Session.UNDEFINED);
Brad Ebinger51b98342016-09-22 16:30:46 -0700259 endParentSessions(subsession);
260 }
261
262 /**
263 * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method
264 * must be called at the end of this method. The full session will complete when all
265 * subsessions are completed.
266 */
267 public synchronized void continueSession(Session subsession, String shortMethodName) {
268 if (subsession == null) {
269 return;
270 }
271 resetStaleSessionTimer();
Brad Ebingera0dc9762016-10-21 09:41:29 -0700272 subsession.setShortMethodName(shortMethodName);
Brad Ebinger51b98342016-09-22 16:30:46 -0700273 subsession.setExecutionStartTimeMs(System.currentTimeMillis());
274 Session parentSession = subsession.getParentSession();
275 if (parentSession == null) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700276 Log.i(LOGGING_TAG, "Log.continueSession was called with no session " +
Brad Ebinger096d2822016-10-13 15:26:58 -0700277 "active for method " + shortMethodName);
Brad Ebinger51b98342016-09-22 16:30:46 -0700278 return;
279 }
280
Brad Ebinger096d2822016-10-13 15:26:58 -0700281 mSessionMapper.put(getCallingThreadId(), subsession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700282 if (!subsession.isStartedFromActiveSession()) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700283 Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
Brad Ebinger51b98342016-09-22 16:30:46 -0700284 } else {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700285 Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700286 " (Invisible Subsession) with Method " + shortMethodName);
287 }
288 }
289
290 /**
291 * Ends the current session/subsession. Must be called after a Log.startSession(...) and
292 * Log.continueSession(...) call.
293 */
294 public synchronized void endSession() {
295 int threadId = getCallingThreadId();
Brad Ebinger096d2822016-10-13 15:26:58 -0700296 Session completedSession = mSessionMapper.get(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700297 if (completedSession == null) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700298 Log.w(LOGGING_TAG, "Log.endSession was called with no session active.");
Brad Ebinger51b98342016-09-22 16:30:46 -0700299 return;
300 }
301
302 completedSession.markSessionCompleted(System.currentTimeMillis());
303 if (!completedSession.isStartedFromActiveSession()) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700304 Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
Brad Ebinger51b98342016-09-22 16:30:46 -0700305 completedSession.getLocalExecutionTime() + " mS)");
306 } else {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700307 Log.v(LOGGING_TAG, Session.END_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700308 " (Invisible Subsession) (dur: " + completedSession.getLocalExecutionTime() +
309 " ms)");
310 }
311 // Remove after completed so that reference still exists for logging the end events
312 Session parentSession = completedSession.getParentSession();
Brad Ebinger096d2822016-10-13 15:26:58 -0700313 mSessionMapper.remove(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700314 endParentSessions(completedSession);
315 // If this subsession was started from a parent session using Log.startSession, return the
316 // ThreadID back to the parent after completion.
317 if (parentSession != null && !parentSession.isSessionCompleted() &&
318 completedSession.isStartedFromActiveSession()) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700319 mSessionMapper.put(threadId, parentSession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700320 }
321 }
322
323 // Recursively deletes all complete parent sessions of the current subsession if it is a leaf.
324 private void endParentSessions(Session subsession) {
325 // Session is not completed or not currently a leaf, so we can not remove because a child is
326 // still running
327 if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) {
328 return;
329 }
Brad Ebinger51b98342016-09-22 16:30:46 -0700330 Session parentSession = subsession.getParentSession();
331 if (parentSession != null) {
332 subsession.setParentSession(null);
333 parentSession.removeChild(subsession);
Brad Ebingera0dc9762016-10-21 09:41:29 -0700334 // Report the child session of the external session as being complete to the listeners,
335 // not the external session itself.
336 if (parentSession.isExternal()) {
337 long fullSessionTimeMs =
338 System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
339 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
340 }
Brad Ebinger51b98342016-09-22 16:30:46 -0700341 endParentSessions(parentSession);
342 } else {
343 // All of the subsessions have been completed and it is time to report on the full
344 // running time of the session.
345 long fullSessionTimeMs =
346 System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
Brad Ebingera0dc9762016-10-21 09:41:29 -0700347 Log.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs
Brad Ebinger51b98342016-09-22 16:30:46 -0700348 + " ms): " + subsession.toString());
Brad Ebingera0dc9762016-10-21 09:41:29 -0700349 if (!subsession.isExternal()) {
350 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs);
Brad Ebinger51b98342016-09-22 16:30:46 -0700351 }
352 }
353 }
354
Brad Ebingera0dc9762016-10-21 09:41:29 -0700355 private void notifySessionCompleteListeners(String methodName, long sessionTimeMs) {
356 for (ISessionListener l : mSessionListeners) {
357 l.sessionComplete(methodName, sessionTimeMs);
358 }
359 }
360
Brad Ebinger51b98342016-09-22 16:30:46 -0700361 public String getSessionId() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700362 Session currentSession = mSessionMapper.get(getCallingThreadId());
Brad Ebinger51b98342016-09-22 16:30:46 -0700363 return currentSession != null ? currentSession.toString() : "";
364 }
365
366 public synchronized void registerSessionListener(ISessionListener l) {
367 if (l != null) {
368 mSessionListeners.add(l);
369 }
370 }
371
372 private synchronized String getNextSessionID() {
373 Integer nextId = sCodeEntryCounter++;
374 if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) {
375 restartSessionCounter();
376 nextId = sCodeEntryCounter++;
377 }
378 return getBase64Encoding(nextId);
379 }
380
Brad Ebingera0dc9762016-10-21 09:41:29 -0700381 private synchronized void restartSessionCounter() {
Brad Ebinger51b98342016-09-22 16:30:46 -0700382 sCodeEntryCounter = 0;
383 }
384
Brad Ebingera0dc9762016-10-21 09:41:29 -0700385 private String getBase64Encoding(int number) {
Brad Ebinger51b98342016-09-22 16:30:46 -0700386 byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array();
387 idByteArray = Arrays.copyOfRange(idByteArray, 2, 4);
388 return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING);
389 }
390
Brad Ebingera0dc9762016-10-21 09:41:29 -0700391 private int getCallingThreadId() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700392 return mCurrentThreadId.get();
Brad Ebinger51b98342016-09-22 16:30:46 -0700393 }
394
395 @VisibleForTesting
Brad Ebingera0dc9762016-10-21 09:41:29 -0700396 public synchronized void cleanupStaleSessions(long timeoutMs) {
Brad Ebinger51b98342016-09-22 16:30:46 -0700397 String logMessage = "Stale Sessions Cleaned:\n";
398 boolean isSessionsStale = false;
399 long currentTimeMs = System.currentTimeMillis();
400 // Remove references that are in the Session Mapper (causing GC to occur) on
401 // sessions that are lasting longer than LOGGING_SESSION_TIMEOUT_MS.
402 // If this occurs, then there is most likely a Session active that never had
403 // Log.endSession called on it.
404 for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it =
Brad Ebinger096d2822016-10-13 15:26:58 -0700405 mSessionMapper.entrySet().iterator(); it.hasNext(); ) {
Brad Ebinger51b98342016-09-22 16:30:46 -0700406 ConcurrentHashMap.Entry<Integer, Session> entry = it.next();
407 Session session = entry.getValue();
408 if (currentTimeMs - session.getExecutionStartTimeMilliseconds() > timeoutMs) {
409 it.remove();
410 logMessage += session.printFullSessionTree() + "\n";
411 isSessionsStale = true;
412 }
413 }
414 if (isSessionsStale) {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700415 Log.w(LOGGING_TAG, logMessage);
Brad Ebinger51b98342016-09-22 16:30:46 -0700416 } else {
Brad Ebingera0dc9762016-10-21 09:41:29 -0700417 Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
Brad Ebinger51b98342016-09-22 16:30:46 -0700418 }
419 }
420
421 /**
422 * Returns the amount of time after a Logging session has been started that Telecom is set to
423 * perform a sweep to check and make sure that the session is still not incomplete (stale).
424 */
425 private long getCleanupTimeout(Context context) {
426 return Settings.Secure.getLong(context.getContentResolver(), TIMEOUTS_PREFIX +
427 "stale_session_cleanup_timeout_millis", DEFAULT_SESSION_TIMEOUT_MS);
428 }
429}