blob: add12374136a2c445277e81cd6b9a60cc884c888 [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;
24import android.util.Base64;
25
26import com.android.internal.annotations.VisibleForTesting;
27
28import java.nio.ByteBuffer;
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.Iterator;
32import java.util.List;
33import java.util.concurrent.ConcurrentHashMap;
34
35/**
36 * TODO: Create better Sessions Documentation
37 * @hide
38 */
39
40public class SessionManager {
41
42 // Currently using 3 letters, So don't exceed 64^3
43 private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144;
44
45 // This parameter can be overridden in Telecom's Timeouts class.
46 public static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds
47
48 private static String LOGGING_TAG = "Logging";
49
50 private static final String TIMEOUTS_PREFIX = "telecom.";
51
52 // Synchronized in all method calls
53 private int sCodeEntryCounter = 0;
54 private Context mContext;
55
56 @VisibleForTesting
Brad Ebinger096d2822016-10-13 15:26:58 -070057 public ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(100);
Brad Ebinger51b98342016-09-22 16:30:46 -070058 @VisibleForTesting
Brad Ebinger096d2822016-10-13 15:26:58 -070059 public Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper());
Brad Ebinger51b98342016-09-22 16:30:46 -070060 @VisibleForTesting
Brad Ebinger096d2822016-10-13 15:26:58 -070061 public java.lang.Runnable mCleanStaleSessions = () ->
Brad Ebinger51b98342016-09-22 16:30:46 -070062 cleanupStaleSessions(getSessionCleanupTimeoutMs());
63
64 // Overridden in LogTest to skip query to ContentProvider
65 private interface ISessionCleanupTimeoutMs {
66 long get();
67 }
68
Brad Ebinger096d2822016-10-13 15:26:58 -070069 // Overridden in tests to provide test Thread IDs
70 public interface ICurrentThreadId {
71 int get();
72 }
73
Brad Ebinger51b98342016-09-22 16:30:46 -070074 @VisibleForTesting
Brad Ebinger096d2822016-10-13 15:26:58 -070075 public ICurrentThreadId mCurrentThreadId = Process::myTid;
76
77 @VisibleForTesting
78 public ISessionCleanupTimeoutMs mSessionCleanupTimeoutMs = () -> {
Brad Ebinger51b98342016-09-22 16:30:46 -070079 // mContext may be null in some cases, such as testing. For these cases, use the
80 // default value.
81 if (mContext == null) {
82 return DEFAULT_SESSION_TIMEOUT_MS;
83 }
84 return getCleanupTimeout(mContext);
85 };
86
87 // Usage is synchronized on this class.
88 private List<ISessionListener> mSessionListeners = new ArrayList<>();
89
90 public interface ISessionListener {
91 /**
92 * This method is run when a full Session has completed.
93 * @param sessionName The name of the Session that has completed.
94 * @param timeMs The time it took to complete in ms.
95 */
96 void sessionComplete(String sessionName, long timeMs);
97 }
98
99 public interface ISessionIdQueryHandler {
100 String getSessionId();
101 }
102
103 public void setContext(Context context) {
104 mContext = context;
105 }
106
107 public SessionManager() {
108 }
109
110 private long getSessionCleanupTimeoutMs() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700111 return mSessionCleanupTimeoutMs.get();
Brad Ebinger51b98342016-09-22 16:30:46 -0700112 }
113
114 private synchronized void resetStaleSessionTimer() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700115 mSessionCleanupHandler.removeCallbacksAndMessages(null);
Brad Ebinger51b98342016-09-22 16:30:46 -0700116 // Will be null in Log Testing
Brad Ebinger096d2822016-10-13 15:26:58 -0700117 if (mCleanStaleSessions != null) {
118 mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs());
Brad Ebinger51b98342016-09-22 16:30:46 -0700119 }
120 }
121
122 /**
123 * Call at an entry point to the Telecom code to track the session. This code must be
124 * accompanied by a Log.endSession().
125 */
126 public synchronized void startSession(String shortMethodName,
127 String callerIdentification) {
128 resetStaleSessionTimer();
129 int threadId = getCallingThreadId();
Brad Ebinger096d2822016-10-13 15:26:58 -0700130 Session activeSession = mSessionMapper.get(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700131 // We have called startSession within an active session that has not ended... Register this
132 // session as a subsession.
133 if (activeSession != null) {
134 Session childSession = createSubsession(true);
135 continueSession(childSession, shortMethodName);
136 return;
137 }
138 Session newSession = new Session(getNextSessionID(), shortMethodName,
139 System.currentTimeMillis(), threadId, false, callerIdentification);
Brad Ebinger096d2822016-10-13 15:26:58 -0700140 mSessionMapper.put(threadId, newSession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700141
Brad Ebinger096d2822016-10-13 15:26:58 -0700142 android.util.Slog.v(LOGGING_TAG, Session.START_SESSION);
Brad Ebinger51b98342016-09-22 16:30:46 -0700143 }
144
145
146 /**
147 * Notifies the logging system that a subsession will be run at a later point and
148 * allocates the resources. Returns a session object that must be used in
149 * Log.continueSession(...) to start the subsession.
150 */
151 public Session createSubsession() {
152 return createSubsession(false);
153 }
154
155 private synchronized Session createSubsession(boolean isStartedFromActiveSession) {
156 int threadId = getCallingThreadId();
Brad Ebinger096d2822016-10-13 15:26:58 -0700157 Session threadSession = mSessionMapper.get(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700158 if (threadSession == null) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700159 android.util.Slog.d(LOGGING_TAG, "Log.createSubsession was called with no session " +
Brad Ebinger51b98342016-09-22 16:30:46 -0700160 "active.");
161 return null;
162 }
163 // Start execution time of the session will be overwritten in continueSession(...).
164 Session newSubsession = new Session(threadSession.getNextChildId(),
165 threadSession.getShortMethodName(), System.currentTimeMillis(), threadId,
166 isStartedFromActiveSession, null);
167 threadSession.addChild(newSubsession);
168 newSubsession.setParentSession(threadSession);
169
170 if (!isStartedFromActiveSession) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700171 android.util.Slog.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " +
Brad Ebinger51b98342016-09-22 16:30:46 -0700172 newSubsession.toString());
173 } else {
Brad Ebinger096d2822016-10-13 15:26:58 -0700174 android.util.Slog.v(LOGGING_TAG, Session.CREATE_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700175 " (Invisible subsession)");
176 }
177 return newSubsession;
178 }
179
180 /**
181 * Cancels a subsession that had Log.createSubsession() called on it, but will never have
182 * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned
Brad Ebinger096d2822016-10-13 15:26:58 -0700183 * gracefully instead of being removed by the mSessionCleanupHandler forcefully later.
Brad Ebinger51b98342016-09-22 16:30:46 -0700184 */
185 public synchronized void cancelSubsession(Session subsession) {
186 if (subsession == null) {
187 return;
188 }
189
Brad Ebinger096d2822016-10-13 15:26:58 -0700190 subsession.markSessionCompleted(Session.UNDEFINED);
Brad Ebinger51b98342016-09-22 16:30:46 -0700191 endParentSessions(subsession);
192 }
193
194 /**
195 * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method
196 * must be called at the end of this method. The full session will complete when all
197 * subsessions are completed.
198 */
199 public synchronized void continueSession(Session subsession, String shortMethodName) {
200 if (subsession == null) {
201 return;
202 }
203 resetStaleSessionTimer();
204 String callingMethodName = subsession.getShortMethodName();
205 subsession.setShortMethodName(callingMethodName + "->" + shortMethodName);
206 subsession.setExecutionStartTimeMs(System.currentTimeMillis());
207 Session parentSession = subsession.getParentSession();
208 if (parentSession == null) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700209 android.util.Slog.d(LOGGING_TAG, "Log.continueSession was called with no session " +
210 "active for method " + shortMethodName);
Brad Ebinger51b98342016-09-22 16:30:46 -0700211 return;
212 }
213
Brad Ebinger096d2822016-10-13 15:26:58 -0700214 mSessionMapper.put(getCallingThreadId(), subsession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700215 if (!subsession.isStartedFromActiveSession()) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700216 android.util.Slog.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION);
Brad Ebinger51b98342016-09-22 16:30:46 -0700217 } else {
Brad Ebinger096d2822016-10-13 15:26:58 -0700218 android.util.Slog.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700219 " (Invisible Subsession) with Method " + shortMethodName);
220 }
221 }
222
223 /**
224 * Ends the current session/subsession. Must be called after a Log.startSession(...) and
225 * Log.continueSession(...) call.
226 */
227 public synchronized void endSession() {
228 int threadId = getCallingThreadId();
Brad Ebinger096d2822016-10-13 15:26:58 -0700229 Session completedSession = mSessionMapper.get(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700230 if (completedSession == null) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700231 android.util.Slog.w(LOGGING_TAG, "Log.endSession was called with no session active.");
Brad Ebinger51b98342016-09-22 16:30:46 -0700232 return;
233 }
234
235 completedSession.markSessionCompleted(System.currentTimeMillis());
236 if (!completedSession.isStartedFromActiveSession()) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700237 android.util.Slog.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " +
Brad Ebinger51b98342016-09-22 16:30:46 -0700238 completedSession.getLocalExecutionTime() + " mS)");
239 } else {
Brad Ebinger096d2822016-10-13 15:26:58 -0700240 android.util.Slog.v(LOGGING_TAG, Session.END_SUBSESSION +
Brad Ebinger51b98342016-09-22 16:30:46 -0700241 " (Invisible Subsession) (dur: " + completedSession.getLocalExecutionTime() +
242 " ms)");
243 }
244 // Remove after completed so that reference still exists for logging the end events
245 Session parentSession = completedSession.getParentSession();
Brad Ebinger096d2822016-10-13 15:26:58 -0700246 mSessionMapper.remove(threadId);
Brad Ebinger51b98342016-09-22 16:30:46 -0700247 endParentSessions(completedSession);
248 // If this subsession was started from a parent session using Log.startSession, return the
249 // ThreadID back to the parent after completion.
250 if (parentSession != null && !parentSession.isSessionCompleted() &&
251 completedSession.isStartedFromActiveSession()) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700252 mSessionMapper.put(threadId, parentSession);
Brad Ebinger51b98342016-09-22 16:30:46 -0700253 }
254 }
255
256 // Recursively deletes all complete parent sessions of the current subsession if it is a leaf.
257 private void endParentSessions(Session subsession) {
258 // Session is not completed or not currently a leaf, so we can not remove because a child is
259 // still running
260 if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) {
261 return;
262 }
263
264 Session parentSession = subsession.getParentSession();
265 if (parentSession != null) {
266 subsession.setParentSession(null);
267 parentSession.removeChild(subsession);
268 endParentSessions(parentSession);
269 } else {
270 // All of the subsessions have been completed and it is time to report on the full
271 // running time of the session.
272 long fullSessionTimeMs =
273 System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds();
Brad Ebinger096d2822016-10-13 15:26:58 -0700274 android.util.Slog.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs
Brad Ebinger51b98342016-09-22 16:30:46 -0700275 + " ms): " + subsession.toString());
Brad Ebinger51b98342016-09-22 16:30:46 -0700276 for (ISessionListener l : mSessionListeners) {
277 l.sessionComplete(subsession.getShortMethodName(), fullSessionTimeMs);
278 }
279 }
280 }
281
282 public String getSessionId() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700283 Session currentSession = mSessionMapper.get(getCallingThreadId());
Brad Ebinger51b98342016-09-22 16:30:46 -0700284 return currentSession != null ? currentSession.toString() : "";
285 }
286
287 public synchronized void registerSessionListener(ISessionListener l) {
288 if (l != null) {
289 mSessionListeners.add(l);
290 }
291 }
292
293 private synchronized String getNextSessionID() {
294 Integer nextId = sCodeEntryCounter++;
295 if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) {
296 restartSessionCounter();
297 nextId = sCodeEntryCounter++;
298 }
299 return getBase64Encoding(nextId);
300 }
301
302 @VisibleForTesting
303 public synchronized void restartSessionCounter() {
304 sCodeEntryCounter = 0;
305 }
306
307 @VisibleForTesting
308 public String getBase64Encoding(int number) {
309 byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array();
310 idByteArray = Arrays.copyOfRange(idByteArray, 2, 4);
311 return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING);
312 }
313
314 public int getCallingThreadId() {
Brad Ebinger096d2822016-10-13 15:26:58 -0700315 return mCurrentThreadId.get();
Brad Ebinger51b98342016-09-22 16:30:46 -0700316 }
317
318 @VisibleForTesting
319 private synchronized void cleanupStaleSessions(long timeoutMs) {
320 String logMessage = "Stale Sessions Cleaned:\n";
321 boolean isSessionsStale = false;
322 long currentTimeMs = System.currentTimeMillis();
323 // Remove references that are in the Session Mapper (causing GC to occur) on
324 // sessions that are lasting longer than LOGGING_SESSION_TIMEOUT_MS.
325 // If this occurs, then there is most likely a Session active that never had
326 // Log.endSession called on it.
327 for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it =
Brad Ebinger096d2822016-10-13 15:26:58 -0700328 mSessionMapper.entrySet().iterator(); it.hasNext(); ) {
Brad Ebinger51b98342016-09-22 16:30:46 -0700329 ConcurrentHashMap.Entry<Integer, Session> entry = it.next();
330 Session session = entry.getValue();
331 if (currentTimeMs - session.getExecutionStartTimeMilliseconds() > timeoutMs) {
332 it.remove();
333 logMessage += session.printFullSessionTree() + "\n";
334 isSessionsStale = true;
335 }
336 }
337 if (isSessionsStale) {
Brad Ebinger096d2822016-10-13 15:26:58 -0700338 android.util.Slog.w(LOGGING_TAG, logMessage);
Brad Ebinger51b98342016-09-22 16:30:46 -0700339 } else {
Brad Ebinger096d2822016-10-13 15:26:58 -0700340 android.util.Slog.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned...");
Brad Ebinger51b98342016-09-22 16:30:46 -0700341 }
342 }
343
344 /**
345 * Returns the amount of time after a Logging session has been started that Telecom is set to
346 * perform a sweep to check and make sure that the session is still not incomplete (stale).
347 */
348 private long getCleanupTimeout(Context context) {
349 return Settings.Secure.getLong(context.getContentResolver(), TIMEOUTS_PREFIX +
350 "stale_session_cleanup_timeout_millis", DEFAULT_SESSION_TIMEOUT_MS);
351 }
352}