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.content.Context; 20 import android.os.Handler; 21 import android.os.Looper; 22 import android.os.Process; 23 import android.provider.Settings; 24 import android.telecom.Log; 25 import android.util.Base64; 26 27 import com.android.internal.annotations.VisibleForTesting; 28 29 import java.nio.ByteBuffer; 30 import java.util.ArrayList; 31 import java.util.Arrays; 32 import java.util.Iterator; 33 import java.util.List; 34 import java.util.concurrent.ConcurrentHashMap; 35 36 /** 37 * TODO: Create better Sessions Documentation 38 * @hide 39 */ 40 41 public class SessionManager { 42 43 // Currently using 3 letters, So don't exceed 64^3 44 private static final long SESSION_ID_ROLLOVER_THRESHOLD = 262144; 45 // This parameter can be overridden in Telecom's Timeouts class. 46 private static final long DEFAULT_SESSION_TIMEOUT_MS = 30000L; // 30 seconds 47 private static final String LOGGING_TAG = "Logging"; 48 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 55 public ConcurrentHashMap<Integer, Session> mSessionMapper = new ConcurrentHashMap<>(100); 56 @VisibleForTesting 57 public java.lang.Runnable mCleanStaleSessions = () -> 58 cleanupStaleSessions(getSessionCleanupTimeoutMs()); 59 private Handler mSessionCleanupHandler = new Handler(Looper.getMainLooper()); 60 61 // Overridden in LogTest to skip query to ContentProvider 62 private interface ISessionCleanupTimeoutMs { get()63 long get(); 64 } 65 66 // Overridden in tests to provide test Thread IDs 67 public interface ICurrentThreadId { get()68 int get(); 69 } 70 71 @VisibleForTesting 72 public ICurrentThreadId mCurrentThreadId = Process::myTid; 73 74 private ISessionCleanupTimeoutMs mSessionCleanupTimeoutMs = () -> { 75 // 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 */ sessionComplete(String sessionName, long timeMs)92 void sessionComplete(String sessionName, long timeMs); 93 } 94 95 public interface ISessionIdQueryHandler { getSessionId()96 String getSessionId(); 97 } 98 setContext(Context context)99 public void setContext(Context context) { 100 mContext = context; 101 } 102 SessionManager()103 public SessionManager() { 104 } 105 getSessionCleanupTimeoutMs()106 private long getSessionCleanupTimeoutMs() { 107 return mSessionCleanupTimeoutMs.get(); 108 } 109 resetStaleSessionTimer()110 private synchronized void resetStaleSessionTimer() { 111 mSessionCleanupHandler.removeCallbacksAndMessages(null); 112 // Will be null in Log Testing 113 if (mCleanStaleSessions != null) { 114 mSessionCleanupHandler.postDelayed(mCleanStaleSessions, getSessionCleanupTimeoutMs()); 115 } 116 } 117 118 /** 119 * 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 */ startSession(Session.Info info, String shortMethodName, String callerIdentification)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 /** 134 * Call at an entry point to the Telecom code to track the session. This code must be 135 * accompanied by a Log.endSession(). 136 */ startSession(String shortMethodName, String callerIdentification)137 public synchronized void startSession(String shortMethodName, 138 String callerIdentification) { 139 resetStaleSessionTimer(); 140 int threadId = getCallingThreadId(); 141 Session activeSession = mSessionMapper.get(threadId); 142 // 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; 148 } else { 149 // Only Log that we are starting the parent session. 150 Log.d(LOGGING_TAG, Session.START_SESSION); 151 } 152 Session newSession = new Session(getNextSessionID(), shortMethodName, 153 System.currentTimeMillis(), false, callerIdentification); 154 mSessionMapper.put(threadId, newSession); 155 } 156 157 /** 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 */ startExternalSession(Session.Info sessionInfo, String shortMethodName)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 Log.d(LOGGING_TAG, Session.START_EXTERNAL_SESSION); 181 Session externalSession = new Session(Session.EXTERNAL_INDICATOR + sessionInfo.sessionId, 182 sessionInfo.methodPath, System.currentTimeMillis(), 183 false /*isStartedFromActiveSession*/, null); 184 externalSession.setIsExternal(true); 185 // Mark the external session as already completed, since we have no way of knowing when 186 // the external session actually has completed. 187 externalSession.markSessionCompleted(Session.UNDEFINED); 188 // Track the external session with the SessionMapper so that we can create and continue 189 // an active subsession based on it. 190 mSessionMapper.put(threadId, externalSession); 191 // Create a subsession from this external Session parent node 192 Session childSession = createSubsession(); 193 continueSession(childSession, shortMethodName); 194 } 195 196 /** 197 * Notifies the logging system that a subsession will be run at a later point and 198 * allocates the resources. Returns a session object that must be used in 199 * Log.continueSession(...) to start the subsession. 200 */ createSubsession()201 public Session createSubsession() { 202 return createSubsession(false); 203 } 204 205 /** 206 * Creates a new subsession based on an existing session. Will not be started until 207 * {@link #continueSession(Session, String)} or {@link #cancelSubsession(Session)} is called. 208 * <p> 209 * Only public for testing! 210 * @param isStartedFromActiveSession true if this subsession is being created for a task on the 211 * same thread, false if it is being created for a related task on another thread. 212 * @return a new {@link Session}, call {@link #continueSession(Session, String)} to continue the 213 * session and {@link #endSession()} when done with this subsession. 214 */ 215 @VisibleForTesting createSubsession(boolean isStartedFromActiveSession)216 public synchronized Session createSubsession(boolean isStartedFromActiveSession) { 217 int threadId = getCallingThreadId(); 218 Session threadSession = mSessionMapper.get(threadId); 219 if (threadSession == null) { 220 Log.d(LOGGING_TAG, "Log.createSubsession was called with no session " + 221 "active."); 222 return null; 223 } 224 // Start execution time of the session will be overwritten in continueSession(...). 225 Session newSubsession = new Session(threadSession.getNextChildId(), 226 threadSession.getShortMethodName(), System.currentTimeMillis(), 227 isStartedFromActiveSession, null); 228 threadSession.addChild(newSubsession); 229 newSubsession.setParentSession(threadSession); 230 231 if (!isStartedFromActiveSession) { 232 Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + " " + 233 newSubsession.toString()); 234 } else { 235 Log.v(LOGGING_TAG, Session.CREATE_SUBSESSION + 236 " (Invisible subsession)"); 237 } 238 return newSubsession; 239 } 240 241 /** 242 * Retrieve the information of the currently active Session. This information is parcelable and 243 * is used to create an external Session ({@link #startExternalSession(Session.Info, String)}). 244 * If there is no Session active, this method will return null. 245 */ getExternalSession()246 public synchronized Session.Info getExternalSession() { 247 int threadId = getCallingThreadId(); 248 Session threadSession = mSessionMapper.get(threadId); 249 if (threadSession == null) { 250 Log.d(LOGGING_TAG, "Log.getExternalSession was called with no session " + 251 "active."); 252 return null; 253 } 254 255 return threadSession.getInfo(); 256 } 257 258 /** 259 * Cancels a subsession that had Log.createSubsession() called on it, but will never have 260 * Log.continueSession(...) called on it due to an error. Allows the subsession to be cleaned 261 * gracefully instead of being removed by the mSessionCleanupHandler forcefully later. 262 */ cancelSubsession(Session subsession)263 public synchronized void cancelSubsession(Session subsession) { 264 if (subsession == null) { 265 return; 266 } 267 268 subsession.markSessionCompleted(Session.UNDEFINED); 269 endParentSessions(subsession); 270 } 271 272 /** 273 * Starts the subsession that was created in Log.CreateSubsession. The Log.endSession() method 274 * must be called at the end of this method. The full session will complete when all 275 * subsessions are completed. 276 */ continueSession(Session subsession, String shortMethodName)277 public synchronized void continueSession(Session subsession, String shortMethodName) { 278 if (subsession == null) { 279 return; 280 } 281 resetStaleSessionTimer(); 282 subsession.setShortMethodName(shortMethodName); 283 subsession.setExecutionStartTimeMs(System.currentTimeMillis()); 284 Session parentSession = subsession.getParentSession(); 285 if (parentSession == null) { 286 Log.i(LOGGING_TAG, "Log.continueSession was called with no session " + 287 "active for method " + shortMethodName); 288 return; 289 } 290 291 mSessionMapper.put(getCallingThreadId(), subsession); 292 if (!subsession.isStartedFromActiveSession()) { 293 Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION); 294 } else { 295 Log.v(LOGGING_TAG, Session.CONTINUE_SUBSESSION + 296 " (Invisible Subsession) with Method " + shortMethodName); 297 } 298 } 299 300 /** 301 * Ends the current session/subsession. Must be called after a Log.startSession(...) and 302 * Log.continueSession(...) call. 303 */ endSession()304 public synchronized void endSession() { 305 int threadId = getCallingThreadId(); 306 Session completedSession = mSessionMapper.get(threadId); 307 if (completedSession == null) { 308 Log.w(LOGGING_TAG, "Log.endSession was called with no session active."); 309 return; 310 } 311 312 completedSession.markSessionCompleted(System.currentTimeMillis()); 313 if (!completedSession.isStartedFromActiveSession()) { 314 Log.v(LOGGING_TAG, Session.END_SUBSESSION + " (dur: " + 315 completedSession.getLocalExecutionTime() + " mS)"); 316 } else { 317 Log.v(LOGGING_TAG, Session.END_SUBSESSION + 318 " (Invisible Subsession) (dur: " + completedSession.getLocalExecutionTime() + 319 " ms)"); 320 } 321 // Remove after completed so that reference still exists for logging the end events 322 Session parentSession = completedSession.getParentSession(); 323 mSessionMapper.remove(threadId); 324 endParentSessions(completedSession); 325 // If this subsession was started from a parent session using Log.startSession, return the 326 // ThreadID back to the parent after completion. 327 if (parentSession != null && !parentSession.isSessionCompleted() && 328 completedSession.isStartedFromActiveSession()) { 329 mSessionMapper.put(threadId, parentSession); 330 } 331 } 332 333 // Recursively deletes all complete parent sessions of the current subsession if it is a leaf. endParentSessions(Session subsession)334 private void endParentSessions(Session subsession) { 335 // Session is not completed or not currently a leaf, so we can not remove because a child is 336 // still running 337 if (!subsession.isSessionCompleted() || subsession.getChildSessions().size() != 0) { 338 return; 339 } 340 Session parentSession = subsession.getParentSession(); 341 if (parentSession != null) { 342 subsession.setParentSession(null); 343 parentSession.removeChild(subsession); 344 // Report the child session of the external session as being complete to the listeners, 345 // not the external session itself. 346 if (parentSession.isExternal()) { 347 long fullSessionTimeMs = 348 System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds(); 349 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs); 350 } 351 endParentSessions(parentSession); 352 } else { 353 // All of the subsessions have been completed and it is time to report on the full 354 // running time of the session. 355 long fullSessionTimeMs = 356 System.currentTimeMillis() - subsession.getExecutionStartTimeMilliseconds(); 357 Log.d(LOGGING_TAG, Session.END_SESSION + " (dur: " + fullSessionTimeMs 358 + " ms): " + subsession.toString()); 359 if (!subsession.isExternal()) { 360 notifySessionCompleteListeners(subsession.getShortMethodName(), fullSessionTimeMs); 361 } 362 } 363 } 364 notifySessionCompleteListeners(String methodName, long sessionTimeMs)365 private void notifySessionCompleteListeners(String methodName, long sessionTimeMs) { 366 for (ISessionListener l : mSessionListeners) { 367 l.sessionComplete(methodName, sessionTimeMs); 368 } 369 } 370 getSessionId()371 public String getSessionId() { 372 Session currentSession = mSessionMapper.get(getCallingThreadId()); 373 return currentSession != null ? currentSession.toString() : ""; 374 } 375 registerSessionListener(ISessionListener l)376 public synchronized void registerSessionListener(ISessionListener l) { 377 if (l != null) { 378 mSessionListeners.add(l); 379 } 380 } 381 getNextSessionID()382 private synchronized String getNextSessionID() { 383 Integer nextId = sCodeEntryCounter++; 384 if (nextId >= SESSION_ID_ROLLOVER_THRESHOLD) { 385 restartSessionCounter(); 386 nextId = sCodeEntryCounter++; 387 } 388 return getBase64Encoding(nextId); 389 } 390 restartSessionCounter()391 private synchronized void restartSessionCounter() { 392 sCodeEntryCounter = 0; 393 } 394 getBase64Encoding(int number)395 private String getBase64Encoding(int number) { 396 byte[] idByteArray = ByteBuffer.allocate(4).putInt(number).array(); 397 idByteArray = Arrays.copyOfRange(idByteArray, 2, 4); 398 return Base64.encodeToString(idByteArray, Base64.NO_WRAP | Base64.NO_PADDING); 399 } 400 getCallingThreadId()401 private int getCallingThreadId() { 402 return mCurrentThreadId.get(); 403 } 404 405 /** 406 * @return A String representation of the active sessions at the time that this method is 407 * called. 408 */ 409 @VisibleForTesting printActiveSessions()410 public synchronized String printActiveSessions() { 411 StringBuilder message = new StringBuilder(); 412 for (ConcurrentHashMap.Entry<Integer, Session> entry : mSessionMapper.entrySet()) { 413 message.append(entry.getValue().printFullSessionTree()); 414 message.append("\n"); 415 } 416 return message.toString(); 417 } 418 419 @VisibleForTesting cleanupStaleSessions(long timeoutMs)420 public synchronized void cleanupStaleSessions(long timeoutMs) { 421 String logMessage = "Stale Sessions Cleaned:\n"; 422 boolean isSessionsStale = false; 423 long currentTimeMs = System.currentTimeMillis(); 424 // Remove references that are in the Session Mapper (causing GC to occur) on 425 // sessions that are lasting longer than LOGGING_SESSION_TIMEOUT_MS. 426 // If this occurs, then there is most likely a Session active that never had 427 // Log.endSession called on it. 428 for (Iterator<ConcurrentHashMap.Entry<Integer, Session>> it = 429 mSessionMapper.entrySet().iterator(); it.hasNext(); ) { 430 ConcurrentHashMap.Entry<Integer, Session> entry = it.next(); 431 Session session = entry.getValue(); 432 if (currentTimeMs - session.getExecutionStartTimeMilliseconds() > timeoutMs) { 433 it.remove(); 434 logMessage += session.printFullSessionTree() + "\n"; 435 isSessionsStale = true; 436 } 437 } 438 if (isSessionsStale) { 439 Log.w(LOGGING_TAG, logMessage); 440 } else { 441 Log.v(LOGGING_TAG, "No stale logging sessions needed to be cleaned..."); 442 } 443 } 444 445 /** 446 * Returns the amount of time after a Logging session has been started that Telecom is set to 447 * perform a sweep to check and make sure that the session is still not incomplete (stale). 448 */ getCleanupTimeout(Context context)449 private long getCleanupTimeout(Context context) { 450 return Settings.Secure.getLong(context.getContentResolver(), TIMEOUTS_PREFIX + 451 "stale_session_cleanup_timeout_millis", DEFAULT_SESSION_TIMEOUT_MS); 452 } 453 } 454