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