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