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