1 /*
2  * Copyright (C) 2020 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 com.android.internal.jank;
18 
19 import static android.Manifest.permission.READ_DEVICE_CONFIG;
20 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
21 import static android.provider.DeviceConfig.NAMESPACE_INTERACTION_JANK_MONITOR;
22 
23 import static com.android.internal.jank.FrameTracker.REASON_CANCEL_NORMAL;
24 import static com.android.internal.jank.FrameTracker.REASON_CANCEL_TIMEOUT;
25 import static com.android.internal.jank.FrameTracker.REASON_END_NORMAL;
26 
27 import android.Manifest;
28 import android.annotation.ColorInt;
29 import android.annotation.NonNull;
30 import android.annotation.RequiresPermission;
31 import android.annotation.UiThread;
32 import android.annotation.WorkerThread;
33 import android.app.ActivityThread;
34 import android.content.Context;
35 import android.graphics.Color;
36 import android.os.Build;
37 import android.os.Handler;
38 import android.os.HandlerExecutor;
39 import android.os.HandlerThread;
40 import android.os.SystemClock;
41 import android.provider.DeviceConfig;
42 import android.text.TextUtils;
43 import android.util.Log;
44 import android.util.SparseArray;
45 import android.view.Choreographer;
46 import android.view.SurfaceControl;
47 import android.view.View;
48 
49 import com.android.internal.annotations.GuardedBy;
50 import com.android.internal.annotations.VisibleForTesting;
51 import com.android.internal.jank.FrameTracker.ChoreographerWrapper;
52 import com.android.internal.jank.FrameTracker.FrameMetricsWrapper;
53 import com.android.internal.jank.FrameTracker.FrameTrackerListener;
54 import com.android.internal.jank.FrameTracker.Reasons;
55 import com.android.internal.jank.FrameTracker.SurfaceControlWrapper;
56 import com.android.internal.jank.FrameTracker.ThreadedRendererWrapper;
57 import com.android.internal.jank.FrameTracker.ViewRootWrapper;
58 import com.android.internal.util.PerfettoTrigger;
59 
60 import java.time.Instant;
61 import java.util.concurrent.ThreadLocalRandom;
62 import java.util.concurrent.TimeUnit;
63 import java.util.function.Supplier;
64 
65 /**
66  * This class lets users begin and end the always on tracing mechanism.
67  *
68  * Enabling for local development:
69  *<pre>
70  * adb shell device_config put interaction_jank_monitor enabled true
71  * adb shell device_config put interaction_jank_monitor sampling_interval 1
72  * </pre>
73  * On debuggable builds, an overlay can be used to display the name of the
74  * currently running cuj using:
75  * <pre>
76  * adb shell device_config put interaction_jank_monitor debug_overlay_enabled true
77  * </pre>
78  * <b>NOTE</b>: The overlay will interfere with metrics, so it should only be used
79  * for understanding which UI events correspond to which CUJs.
80  *
81  * @hide
82  */
83 public class InteractionJankMonitor {
84     private static final String TAG = InteractionJankMonitor.class.getSimpleName();
85     private static final String ACTION_PREFIX = InteractionJankMonitor.class.getCanonicalName();
86 
87     private static final String DEFAULT_WORKER_NAME = TAG + "-Worker";
88     private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(2L);
89     static final long EXECUTOR_TASK_TIMEOUT = 500;
90     private static final String SETTINGS_ENABLED_KEY = "enabled";
91     private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
92     private static final String SETTINGS_THRESHOLD_MISSED_FRAMES_KEY =
93             "trace_threshold_missed_frames";
94     private static final String SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY =
95             "trace_threshold_frame_time_millis";
96     private static final String SETTINGS_DEBUG_OVERLAY_ENABLED_KEY = "debug_overlay_enabled";
97     /** Default to being enabled on debug builds. */
98     private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE;
99     /** Default to collecting data for all CUJs. */
100     private static final int DEFAULT_SAMPLING_INTERVAL = 1;
101     /** Default to triggering trace if 3 frames are missed OR a frame takes at least 64ms */
102     private static final int DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES = 3;
103     private static final int DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS = 64;
104     private static final boolean DEFAULT_DEBUG_OVERLAY_ENABLED = false;
105 
106     private static final int MAX_LENGTH_SESSION_NAME = 100;
107 
108     public static final String ACTION_SESSION_END = ACTION_PREFIX + ".ACTION_SESSION_END";
109     public static final String ACTION_SESSION_CANCEL = ACTION_PREFIX + ".ACTION_SESSION_CANCEL";
110 
111     // These are not the CUJ constants you are looking for. These constants simply forward their
112     // definition from {@link Cuj}. They are here only as a transition measure until all references
113     // have been updated to the new location.
114     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE = Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE;
115     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_SCROLL_FLING = Cuj.CUJ_NOTIFICATION_SHADE_SCROLL_FLING;
116     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_ROW_EXPAND = Cuj.CUJ_NOTIFICATION_SHADE_ROW_EXPAND;
117     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_ROW_SWIPE = Cuj.CUJ_NOTIFICATION_SHADE_ROW_SWIPE;
118     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE = Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE;
119     @Deprecated public static final int CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE = Cuj.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE;
120     @Deprecated public static final int CUJ_NOTIFICATION_HEADS_UP_APPEAR = Cuj.CUJ_NOTIFICATION_HEADS_UP_APPEAR;
121     @Deprecated public static final int CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR = Cuj.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR;
122     @Deprecated public static final int CUJ_NOTIFICATION_ADD = Cuj.CUJ_NOTIFICATION_ADD;
123     @Deprecated public static final int CUJ_NOTIFICATION_REMOVE = Cuj.CUJ_NOTIFICATION_REMOVE;
124     @Deprecated public static final int CUJ_NOTIFICATION_APP_START = Cuj.CUJ_NOTIFICATION_APP_START;
125     @Deprecated public static final int CUJ_LOCKSCREEN_PASSWORD_APPEAR = Cuj.CUJ_LOCKSCREEN_PASSWORD_APPEAR;
126     @Deprecated public static final int CUJ_LOCKSCREEN_PATTERN_APPEAR = Cuj.CUJ_LOCKSCREEN_PATTERN_APPEAR;
127     @Deprecated public static final int CUJ_LOCKSCREEN_PIN_APPEAR = Cuj.CUJ_LOCKSCREEN_PIN_APPEAR;
128     @Deprecated public static final int CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR = Cuj.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR;
129     @Deprecated public static final int CUJ_LOCKSCREEN_PATTERN_DISAPPEAR = Cuj.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR;
130     @Deprecated public static final int CUJ_LOCKSCREEN_PIN_DISAPPEAR = Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR;
131     @Deprecated public static final int CUJ_LOCKSCREEN_TRANSITION_FROM_AOD = Cuj.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
132     @Deprecated public static final int CUJ_LOCKSCREEN_TRANSITION_TO_AOD = Cuj.CUJ_LOCKSCREEN_TRANSITION_TO_AOD;
133     @Deprecated public static final int CUJ_SETTINGS_PAGE_SCROLL = Cuj.CUJ_SETTINGS_PAGE_SCROLL;
134     @Deprecated public static final int CUJ_LOCKSCREEN_UNLOCK_ANIMATION = Cuj.CUJ_LOCKSCREEN_UNLOCK_ANIMATION;
135     @Deprecated public static final int CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON = Cuj.CUJ_SHADE_APP_LAUNCH_FROM_HISTORY_BUTTON;
136     @Deprecated public static final int CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER = Cuj.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER;
137     @Deprecated public static final int CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE = Cuj.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE;
138     @Deprecated public static final int CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON = Cuj.CUJ_SHADE_APP_LAUNCH_FROM_SETTINGS_BUTTON;
139     @Deprecated public static final int CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP = Cuj.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP;
140     @Deprecated public static final int CUJ_PIP_TRANSITION = Cuj.CUJ_PIP_TRANSITION;
141     @Deprecated public static final int CUJ_USER_SWITCH = Cuj.CUJ_USER_SWITCH;
142     @Deprecated public static final int CUJ_SPLASHSCREEN_AVD = Cuj.CUJ_SPLASHSCREEN_AVD;
143     @Deprecated public static final int CUJ_SPLASHSCREEN_EXIT_ANIM = Cuj.CUJ_SPLASHSCREEN_EXIT_ANIM;
144     @Deprecated public static final int CUJ_SCREEN_OFF = Cuj.CUJ_SCREEN_OFF;
145     @Deprecated public static final int CUJ_SCREEN_OFF_SHOW_AOD = Cuj.CUJ_SCREEN_OFF_SHOW_AOD;
146     @Deprecated public static final int CUJ_UNFOLD_ANIM = Cuj.CUJ_UNFOLD_ANIM;
147     @Deprecated public static final int CUJ_SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS = Cuj.CUJ_SUW_LOADING_TO_SHOW_INFO_WITH_ACTIONS;
148     @Deprecated public static final int CUJ_SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS = Cuj.CUJ_SUW_SHOW_FUNCTION_SCREEN_WITH_ACTIONS;
149     @Deprecated public static final int CUJ_SUW_LOADING_TO_NEXT_FLOW = Cuj.CUJ_SUW_LOADING_TO_NEXT_FLOW;
150     @Deprecated public static final int CUJ_SUW_LOADING_SCREEN_FOR_STATUS = Cuj.CUJ_SUW_LOADING_SCREEN_FOR_STATUS;
151     @Deprecated public static final int CUJ_SPLIT_SCREEN_RESIZE = Cuj.CUJ_SPLIT_SCREEN_RESIZE;
152     @Deprecated public static final int CUJ_SETTINGS_SLIDER = Cuj.CUJ_SETTINGS_SLIDER;
153     @Deprecated public static final int CUJ_TAKE_SCREENSHOT = Cuj.CUJ_TAKE_SCREENSHOT;
154     @Deprecated public static final int CUJ_VOLUME_CONTROL = Cuj.CUJ_VOLUME_CONTROL;
155     @Deprecated public static final int CUJ_BIOMETRIC_PROMPT_TRANSITION = Cuj.CUJ_BIOMETRIC_PROMPT_TRANSITION;
156     @Deprecated public static final int CUJ_SETTINGS_TOGGLE = Cuj.CUJ_SETTINGS_TOGGLE;
157     @Deprecated public static final int CUJ_SHADE_DIALOG_OPEN = Cuj.CUJ_SHADE_DIALOG_OPEN;
158     @Deprecated public static final int CUJ_USER_DIALOG_OPEN = Cuj.CUJ_USER_DIALOG_OPEN;
159     @Deprecated public static final int CUJ_TASKBAR_EXPAND = Cuj.CUJ_TASKBAR_EXPAND;
160     @Deprecated public static final int CUJ_TASKBAR_COLLAPSE = Cuj.CUJ_TASKBAR_COLLAPSE;
161     @Deprecated public static final int CUJ_SHADE_CLEAR_ALL = Cuj.CUJ_SHADE_CLEAR_ALL;
162     @Deprecated public static final int CUJ_LOCKSCREEN_OCCLUSION = Cuj.CUJ_LOCKSCREEN_OCCLUSION;
163     @Deprecated public static final int CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION = Cuj.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION;
164     @Deprecated public static final int CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER = Cuj.CUJ_SPLIT_SCREEN_DOUBLE_TAP_DIVIDER;
165     @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY = Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
166     @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = Cuj.CUJ_PREDICTIVE_BACK_CROSS_TASK;
167     @Deprecated public static final int CUJ_PREDICTIVE_BACK_HOME = Cuj.CUJ_PREDICTIVE_BACK_HOME;
168     @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE;
169     @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR;
170     @Deprecated public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR;
171     @Deprecated public static final int CUJ_LAUNCHER_ALL_APPS_SEARCH_BACK = Cuj.CUJ_LAUNCHER_ALL_APPS_SEARCH_BACK;
172     @Deprecated public static final int CUJ_LAUNCHER_TASKBAR_ALL_APPS_CLOSE_BACK = Cuj.CUJ_LAUNCHER_TASKBAR_ALL_APPS_CLOSE_BACK;
173     @Deprecated public static final int CUJ_LAUNCHER_TASKBAR_ALL_APPS_SEARCH_BACK = Cuj.CUJ_LAUNCHER_TASKBAR_ALL_APPS_SEARCH_BACK;
174     @Deprecated public static final int CUJ_LAUNCHER_WIDGET_PICKER_CLOSE_BACK = Cuj.CUJ_LAUNCHER_WIDGET_PICKER_CLOSE_BACK;
175     @Deprecated public static final int CUJ_LAUNCHER_WIDGET_PICKER_SEARCH_BACK = Cuj.CUJ_LAUNCHER_WIDGET_PICKER_SEARCH_BACK;
176     @Deprecated public static final int CUJ_LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK = Cuj.CUJ_LAUNCHER_WIDGET_BOTTOM_SHEET_CLOSE_BACK;
177     @Deprecated public static final int CUJ_LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK = Cuj.CUJ_LAUNCHER_WIDGET_EDU_SHEET_CLOSE_BACK;
178 
179     private static class InstanceHolder {
180         public static final InteractionJankMonitor INSTANCE =
181             new InteractionJankMonitor(new HandlerThread(DEFAULT_WORKER_NAME));
182     }
183 
184     @GuardedBy("mLock")
185     private final SparseArray<RunningTracker> mRunningTrackers = new SparseArray<>();
186     private final Handler mWorker;
187     private final DisplayResolutionTracker mDisplayResolutionTracker;
188     private final Object mLock = new Object();
189     private @ColorInt int mDebugBgColor = Color.CYAN;
190     private double mDebugYOffset = 0.1;
191     private InteractionMonitorDebugOverlay mDebugOverlay;
192 
193     private volatile boolean mEnabled = DEFAULT_ENABLED;
194     private int mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
195     private int mTraceThresholdMissedFrames = DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES;
196     private int mTraceThresholdFrameTimeMillis = DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS;
197 
198     /**
199      * Get the singleton of InteractionJankMonitor.
200      *
201      * @return instance of InteractionJankMonitor
202      */
getInstance()203     public static InteractionJankMonitor getInstance() {
204         return InstanceHolder.INSTANCE;
205     }
206 
207     /**
208      * This constructor should be only public to tests.
209      *
210      * @param worker the worker thread for the callbacks
211      */
212     @VisibleForTesting
213     @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG)
InteractionJankMonitor(@onNull HandlerThread worker)214     public InteractionJankMonitor(@NonNull HandlerThread worker) {
215         worker.start();
216         mWorker = worker.getThreadHandler();
217         mDisplayResolutionTracker = new DisplayResolutionTracker(mWorker);
218 
219         final Context context = ActivityThread.currentApplication();
220         if (context == null || context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG) != PERMISSION_GRANTED) {
221             Log.w(TAG, "Initializing without READ_DEVICE_CONFIG permission."
222                     + " enabled=" + mEnabled + ", interval=" + mSamplingInterval
223                     + ", missedFrameThreshold=" + mTraceThresholdMissedFrames
224                     + ", frameTimeThreshold=" + mTraceThresholdFrameTimeMillis
225                     + ", package=" + (context == null ? "null" : context.getPackageName()));
226             return;
227         }
228 
229         // Post initialization to the background in case we're running on the main thread.
230         mWorker.post(() -> {
231             try {
232                 updateProperties(DeviceConfig.getProperties(NAMESPACE_INTERACTION_JANK_MONITOR));
233                 DeviceConfig.addOnPropertiesChangedListener(NAMESPACE_INTERACTION_JANK_MONITOR,
234                         new HandlerExecutor(mWorker), this::updateProperties);
235             } catch (SecurityException ex) {
236                 Log.d(TAG, "Can't get properties: READ_DEVICE_CONFIG granted="
237                         + context.checkCallingOrSelfPermission(READ_DEVICE_CONFIG)
238                         + ", package=" + context.getPackageName());
239             }
240         });
241     }
242 
243     /**
244      * Creates a {@link FrameTracker} instance.
245      *
246      * @param config the conifg associates with this tracker
247      * @return instance of the FrameTracker
248      */
249     @VisibleForTesting
createFrameTracker(Configuration config)250     public FrameTracker createFrameTracker(Configuration config) {
251         final View view = config.mView;
252 
253         final ThreadedRendererWrapper threadedRenderer =
254                 view == null ? null : new ThreadedRendererWrapper(view.getThreadedRenderer());
255         final ViewRootWrapper viewRoot =
256                 view == null ? null : new ViewRootWrapper(view.getViewRootImpl());
257         final SurfaceControlWrapper surfaceControl = new SurfaceControlWrapper();
258         final ChoreographerWrapper choreographer =
259                 new ChoreographerWrapper(Choreographer.getInstance());
260         final FrameTrackerListener eventsListener = new FrameTrackerListener() {
261             @Override
262             public void onCujEvents(FrameTracker tracker, String action, int reason) {
263                 config.getHandler().runWithScissors(() ->
264                         handleCujEvents(config.mCujType, tracker, action, reason),
265                         EXECUTOR_TASK_TIMEOUT);
266             }
267 
268             @Override
269             public void triggerPerfetto(Configuration config) {
270                 mWorker.post(() -> PerfettoTrigger.trigger(config.getPerfettoTrigger()));
271             }
272         };
273         final FrameMetricsWrapper frameMetrics = new FrameMetricsWrapper();
274 
275         return new FrameTracker(config, threadedRenderer, viewRoot,
276                 surfaceControl, choreographer, frameMetrics,
277                 new FrameTracker.StatsLogWrapper(mDisplayResolutionTracker),
278                 mTraceThresholdMissedFrames, mTraceThresholdFrameTimeMillis,
279                 eventsListener);
280     }
281 
282     @UiThread
handleCujEvents( @uj.CujType int cuj, FrameTracker tracker, String action, @Reasons int reason)283     private void handleCujEvents(
284             @Cuj.CujType int cuj, FrameTracker tracker, String action, @Reasons int reason) {
285         // Clear the running and timeout tasks if the end / cancel was fired within the tracker.
286         // Or we might have memory leaks.
287         if (needRemoveTasks(action, reason)) {
288             removeTrackerIfCurrent(cuj, tracker, reason);
289         }
290     }
291 
needRemoveTasks(String action, @Reasons int reason)292     private static boolean needRemoveTasks(String action, @Reasons int reason) {
293         final boolean badEnd = action.equals(ACTION_SESSION_END) && reason != REASON_END_NORMAL;
294         final boolean badCancel = action.equals(ACTION_SESSION_CANCEL)
295                 && !(reason == REASON_CANCEL_NORMAL || reason == REASON_CANCEL_TIMEOUT);
296         return badEnd || badCancel;
297     }
298 
299     /**
300      * @param cujType cuj type
301      * @return true if the cuj is under instrumenting, false otherwise.
302      */
isInstrumenting(@uj.CujType int cujType)303     public boolean isInstrumenting(@Cuj.CujType int cujType) {
304         synchronized (mLock) {
305             return mRunningTrackers.contains(cujType);
306         }
307     }
308 
309     /**
310      * Begins a trace session.
311      *
312      * @param v an attached view.
313      * @param cujType the specific {@link Cuj.CujType}.
314      * @return boolean true if the tracker is started successfully, false otherwise.
315      */
begin(View v, @Cuj.CujType int cujType)316     public boolean begin(View v, @Cuj.CujType int cujType) {
317         try {
318             return begin(Configuration.Builder.withView(cujType, v));
319         } catch (IllegalArgumentException ex) {
320             Log.d(TAG, "Build configuration failed!", ex);
321             return false;
322         }
323     }
324 
325     /**
326      * Begins a trace session.
327      *
328      * @param builder the builder of the configurations for instrumenting the CUJ.
329      * @return boolean true if the tracker is begun successfully, false otherwise.
330      */
begin(@onNull Configuration.Builder builder)331     public boolean begin(@NonNull Configuration.Builder builder) {
332         try {
333             final Configuration config = builder.build();
334             postEventLogToWorkerThread((unixNanos, elapsedNanos, realtimeNanos) -> {
335                 EventLogTags.writeJankCujEventsBeginRequest(
336                         config.mCujType, unixNanos, elapsedNanos, realtimeNanos, config.mTag);
337             });
338             final TrackerResult result = new TrackerResult();
339             final boolean success = config.getHandler().runWithScissors(
340                     () -> result.mResult = beginInternal(config), EXECUTOR_TASK_TIMEOUT);
341             if (!success) {
342                 Log.d(TAG, "begin failed due to timeout, CUJ=" + Cuj.getNameOfCuj(config.mCujType));
343                 return false;
344             }
345             return result.mResult;
346         } catch (IllegalArgumentException ex) {
347             Log.d(TAG, "Build configuration failed!", ex);
348             return false;
349         }
350     }
351 
352     @UiThread
beginInternal(@onNull Configuration conf)353     private boolean beginInternal(@NonNull Configuration conf) {
354         int cujType = conf.mCujType;
355         if (!shouldMonitor()) {
356             return false;
357         }
358 
359         RunningTracker tracker = putTrackerIfNoCurrent(cujType, () ->
360                 new RunningTracker(
361                     conf, createFrameTracker(conf), () -> cancel(cujType, REASON_CANCEL_TIMEOUT)));
362         if (tracker == null) {
363             return false;
364         }
365 
366         tracker.mTracker.begin();
367         // Cancel the trace if we don't get an end() call in specified duration.
368         scheduleTimeoutAction(tracker.mConfig, tracker.mTimeoutAction);
369 
370         return true;
371     }
372 
373     /**
374      * Check if the monitoring is enabled and if it should be sampled.
375      */
376     @VisibleForTesting
shouldMonitor()377     public boolean shouldMonitor() {
378         return mEnabled && (ThreadLocalRandom.current().nextInt(mSamplingInterval) == 0);
379     }
380 
381     @VisibleForTesting
scheduleTimeoutAction(Configuration config, Runnable action)382     public void scheduleTimeoutAction(Configuration config, Runnable action) {
383         config.getHandler().postDelayed(action, config.mTimeout);
384     }
385 
386     /**
387      * Ends a trace session.
388      *
389      * @param cujType the specific {@link Cuj.CujType}.
390      * @return boolean true if the tracker is ended successfully, false otherwise.
391      */
end(@uj.CujType int cujType)392     public boolean end(@Cuj.CujType int cujType) {
393         postEventLogToWorkerThread((unixNanos, elapsedNanos, realtimeNanos) -> {
394             EventLogTags.writeJankCujEventsEndRequest(
395                     cujType, unixNanos, elapsedNanos, realtimeNanos);
396         });
397         RunningTracker tracker = getTracker(cujType);
398         // Skip this call since we haven't started a trace yet.
399         if (tracker == null) {
400             return false;
401         }
402         try {
403             final TrackerResult result = new TrackerResult();
404             final boolean success = tracker.mConfig.getHandler().runWithScissors(
405                     () -> result.mResult = endInternal(tracker), EXECUTOR_TASK_TIMEOUT);
406             if (!success) {
407                 Log.d(TAG, "end failed due to timeout, CUJ=" + Cuj.getNameOfCuj(cujType));
408                 return false;
409             }
410             return result.mResult;
411         } catch (IllegalArgumentException ex) {
412             Log.d(TAG, "Execute end task failed!", ex);
413             return false;
414         }
415     }
416 
417     @UiThread
endInternal(RunningTracker tracker)418     private boolean endInternal(RunningTracker tracker) {
419         if (removeTrackerIfCurrent(tracker, REASON_END_NORMAL)) {
420             return false;
421         }
422         tracker.mTracker.end(REASON_END_NORMAL);
423         return true;
424     }
425 
426     /**
427      * Cancels the trace session.
428      *
429      * @return boolean true if the tracker is cancelled successfully, false otherwise.
430      */
cancel(@uj.CujType int cujType)431     public boolean cancel(@Cuj.CujType int cujType) {
432         postEventLogToWorkerThread((unixNanos, elapsedNanos, realtimeNanos) -> {
433             EventLogTags.writeJankCujEventsCancelRequest(
434                     cujType, unixNanos, elapsedNanos, realtimeNanos);
435         });
436         return cancel(cujType, REASON_CANCEL_NORMAL);
437     }
438 
439     /**
440      * Cancels the trace session.
441      *
442      * @return boolean true if the tracker is cancelled successfully, false otherwise.
443      */
444     @VisibleForTesting
cancel(@uj.CujType int cujType, @Reasons int reason)445     public boolean cancel(@Cuj.CujType int cujType, @Reasons int reason) {
446         RunningTracker tracker = getTracker(cujType);
447         // Skip this call since we haven't started a trace yet.
448         if (tracker == null) {
449             return false;
450         }
451         try {
452             final TrackerResult result = new TrackerResult();
453             final boolean success = tracker.mConfig.getHandler().runWithScissors(
454                     () -> result.mResult = cancelInternal(tracker, reason), EXECUTOR_TASK_TIMEOUT);
455             if (!success) {
456                 Log.d(TAG, "cancel failed due to timeout, CUJ=" + Cuj.getNameOfCuj(cujType));
457                 return false;
458             }
459             return result.mResult;
460         } catch (IllegalArgumentException ex) {
461             Log.d(TAG, "Execute cancel task failed!", ex);
462             return false;
463         }
464     }
465 
466     @UiThread
cancelInternal(RunningTracker tracker, @Reasons int reason)467     private boolean cancelInternal(RunningTracker tracker, @Reasons int reason) {
468         if (removeTrackerIfCurrent(tracker, reason)) {
469             return false;
470         }
471         tracker.mTracker.cancel(reason);
472         return true;
473     }
474 
475     @UiThread
putTrackerIfNoCurrent( @uj.CujType int cuj, Supplier<RunningTracker> supplier)476     private RunningTracker putTrackerIfNoCurrent(
477             @Cuj.CujType int cuj, Supplier<RunningTracker> supplier) {
478         synchronized (mLock) {
479             if (mRunningTrackers.contains(cuj)) {
480                 return null;
481             }
482 
483             RunningTracker tracker = supplier.get();
484             if (tracker == null) {
485                 return null;
486             }
487 
488             mRunningTrackers.put(cuj, tracker);
489             if (mDebugOverlay != null) {
490                 mDebugOverlay.onTrackerAdded(cuj, tracker);
491             }
492 
493             return tracker;
494         }
495     }
496 
getTracker(@uj.CujType int cuj)497     private RunningTracker getTracker(@Cuj.CujType int cuj) {
498         synchronized (mLock) {
499             return mRunningTrackers.get(cuj);
500         }
501     }
502 
503     /**
504      * @return {@code true} if another tracker is current
505      */
506     @UiThread
removeTrackerIfCurrent(RunningTracker tracker, int reason)507     private boolean removeTrackerIfCurrent(RunningTracker tracker, int reason) {
508         return removeTrackerIfCurrent(tracker.mConfig.mCujType, tracker.mTracker, reason);
509     }
510 
511     /**
512      * @return {@code true} if another tracker is current
513      */
514     @UiThread
removeTrackerIfCurrent(@uj.CujType int cuj, FrameTracker tracker, int reason)515     private boolean removeTrackerIfCurrent(@Cuj.CujType int cuj, FrameTracker tracker, int reason) {
516         synchronized (mLock) {
517             RunningTracker running = mRunningTrackers.get(cuj);
518             if (running == null || running.mTracker != tracker) {
519                 return true;
520             }
521 
522             running.mConfig.getHandler().removeCallbacks(running.mTimeoutAction);
523             mRunningTrackers.remove(cuj);
524             if (mDebugOverlay != null) {
525                 mDebugOverlay.onTrackerRemoved(cuj, reason, mRunningTrackers);
526             }
527             return false;
528         }
529     }
530 
531     @WorkerThread
532     @VisibleForTesting
updateProperties(DeviceConfig.Properties properties)533     public void updateProperties(DeviceConfig.Properties properties) {
534         for (String property : properties.getKeyset()) {
535             switch (property) {
536                 case SETTINGS_SAMPLING_INTERVAL_KEY ->
537                         mSamplingInterval = properties.getInt(property, DEFAULT_SAMPLING_INTERVAL);
538                 case SETTINGS_THRESHOLD_MISSED_FRAMES_KEY ->
539                         mTraceThresholdMissedFrames =
540                                 properties.getInt(property, DEFAULT_TRACE_THRESHOLD_MISSED_FRAMES);
541                 case SETTINGS_THRESHOLD_FRAME_TIME_MILLIS_KEY ->
542                         mTraceThresholdFrameTimeMillis =
543                                 properties.getInt(property, DEFAULT_TRACE_THRESHOLD_FRAME_TIME_MILLIS);
544                 case SETTINGS_ENABLED_KEY ->
545                         mEnabled = properties.getBoolean(property, DEFAULT_ENABLED);
546                 case SETTINGS_DEBUG_OVERLAY_ENABLED_KEY -> {
547                     // Never allow the debug overlay to be used on user builds
548                     boolean debugOverlayEnabled = Build.IS_DEBUGGABLE
549                             && properties.getBoolean(property, DEFAULT_DEBUG_OVERLAY_ENABLED);
550                     if (debugOverlayEnabled && mDebugOverlay == null) {
551                         mDebugOverlay = new InteractionMonitorDebugOverlay(
552                                 mLock, mDebugBgColor, mDebugYOffset);
553                     } else if (!debugOverlayEnabled && mDebugOverlay != null) {
554                         mDebugOverlay.dispose();
555                         mDebugOverlay = null;
556                     }
557                 }
558                 default -> Log.w(TAG, "Got a change event for an unknown property: "
559                         + property + " => " + properties.getString(property, ""));
560             }
561         }
562     }
563 
564     /**
565      * A helper method to translate interaction type to CUJ name.
566      *
567      * @param interactionType the interaction type defined in AtomsProto.java
568      * @return the name of the interaction type
569      * @deprecated use {@link Cuj#getNameOfInteraction(int)}
570      */
571     @Deprecated
getNameOfInteraction(int interactionType)572     public static String getNameOfInteraction(int interactionType) {
573         return Cuj.getNameOfInteraction(interactionType);
574     }
575 
576     /**
577      * A helper method to translate CUJ type to CUJ name.
578      *
579      * @param cujType the cuj type defined in this file
580      * @return the name of the cuj type
581      * @deprecated use {@link Cuj#getNameOfCuj(int)}
582      */
583     @Deprecated
getNameOfCuj(int cujType)584     public static String getNameOfCuj(int cujType) {
585         return Cuj.getNameOfCuj(cujType);
586     }
587 
588     /**
589      * Configures the debug overlay used for displaying interaction names on the screen while they
590      * occur.
591      *
592      * @param bgColor the background color of the box used to display the CUJ names
593      * @param yOffset number between 0 and 1 to indicate where the top of the box should be relative
594      *                to the height of the screen
595      */
configDebugOverlay(@olorInt int bgColor, double yOffset)596     public void configDebugOverlay(@ColorInt int bgColor, double yOffset) {
597         mDebugBgColor = bgColor;
598         mDebugYOffset = yOffset;
599     }
600 
postEventLogToWorkerThread(TimeFunction logFunction)601     private void postEventLogToWorkerThread(TimeFunction logFunction) {
602         final Instant now = Instant.now();
603         final long unixNanos = TimeUnit.NANOSECONDS.convert(now.getEpochSecond(), TimeUnit.SECONDS)
604                 + now.getNano();
605         final long elapsedNanos = SystemClock.elapsedRealtimeNanos();
606         final long realtimeNanos = SystemClock.uptimeNanos();
607 
608         mWorker.post(() -> logFunction.invoke(unixNanos, elapsedNanos, realtimeNanos));
609     }
610 
611     private static class TrackerResult {
612         private boolean mResult;
613     }
614 
615     /**
616      * Configurations used while instrumenting the CUJ. <br/>
617      * <b>It may refer to an attached view, don't use static reference for any purpose.</b>
618      */
619     public static class Configuration {
620         private final View mView;
621         private final Context mContext;
622         private final long mTimeout;
623         private final String mTag;
624         private final String mSessionName;
625         private final boolean mSurfaceOnly;
626         private final SurfaceControl mSurfaceControl;
627         private final @Cuj.CujType int mCujType;
628         private final boolean mDeferMonitor;
629         private final Handler mHandler;
630 
631         /**
632          * A builder for building Configuration. {@link #setView(View)} is essential
633          * if {@link #setSurfaceOnly(boolean)} is not set, otherwise both
634          * {@link #setSurfaceControl(SurfaceControl)} and {@link #setContext(Context)}
635          * are necessary<br/>
636          * <b>It may refer to an attached view, don't use static reference for any purpose.</b>
637          */
638         public static class Builder {
639             private View mAttrView = null;
640             private Context mAttrContext = null;
641             private long mAttrTimeout = DEFAULT_TIMEOUT_MS;
642             private String mAttrTag = "";
643             private boolean mAttrSurfaceOnly;
644             private SurfaceControl mAttrSurfaceControl;
645             private final @Cuj.CujType int mAttrCujType;
646             private boolean mAttrDeferMonitor = true;
647 
648             /**
649              * Creates a builder which instruments only surface.
650              * @param cuj The enum defined in {@link Cuj.CujType}.
651              * @param context context
652              * @param surfaceControl surface control
653              * @return builder
654              */
withSurface(@uj.CujType int cuj, @NonNull Context context, @NonNull SurfaceControl surfaceControl)655             public static Builder withSurface(@Cuj.CujType int cuj, @NonNull Context context,
656                     @NonNull SurfaceControl surfaceControl) {
657                 return new Builder(cuj)
658                         .setContext(context)
659                         .setSurfaceControl(surfaceControl)
660                         .setSurfaceOnly(true);
661             }
662 
663             /**
664              * Creates a builder which instruments both surface and view.
665              * @param cuj The enum defined in {@link Cuj.CujType}.
666              * @param view view
667              * @return builder
668              */
withView(@uj.CujType int cuj, @NonNull View view)669             public static Builder withView(@Cuj.CujType int cuj, @NonNull View view) {
670                 return new Builder(cuj)
671                         .setView(view)
672                         .setContext(view.getContext());
673             }
674 
Builder(@uj.CujType int cuj)675             private Builder(@Cuj.CujType int cuj) {
676                 mAttrCujType = cuj;
677             }
678 
679             /**
680              * Specifies a view, must be set if {@link #setSurfaceOnly(boolean)} is set to false.
681              * @param view an attached view
682              * @return builder
683              */
setView(@onNull View view)684             private Builder setView(@NonNull View view) {
685                 mAttrView = view;
686                 return this;
687             }
688 
689             /**
690              * @param timeout duration to cancel the instrumentation in ms
691              * @return builder
692              */
setTimeout(long timeout)693             public Builder setTimeout(long timeout) {
694                 mAttrTimeout = timeout;
695                 return this;
696             }
697 
698             /**
699              * @param tag The postfix of the CUJ in the output trace.
700              *           It provides a brief description for the CUJ like the concrete class
701              *           who is dealing with the CUJ or the important state with the CUJ, etc.
702              * @return builder
703              */
setTag(@onNull String tag)704             public Builder setTag(@NonNull String tag) {
705                 mAttrTag = tag;
706                 return this;
707             }
708 
709             /**
710              * Indicates if only instrument with surface,
711              * if true, must also setup with {@link #setContext(Context)}
712              * and {@link #setSurfaceControl(SurfaceControl)}.
713              * @param surfaceOnly true if only instrument with surface, false otherwise
714              * @return builder Surface only builder.
715              */
setSurfaceOnly(boolean surfaceOnly)716             private Builder setSurfaceOnly(boolean surfaceOnly) {
717                 mAttrSurfaceOnly = surfaceOnly;
718                 return this;
719             }
720 
721             /**
722              * Specifies a context, must set if {@link #setSurfaceOnly(boolean)} is set.
723              */
setContext(Context context)724             private Builder setContext(Context context) {
725                 mAttrContext = context;
726                 return this;
727             }
728 
729             /**
730              * Specifies a surface control, must be set if {@link #setSurfaceOnly(boolean)} is set.
731              */
setSurfaceControl(SurfaceControl surfaceControl)732             private Builder setSurfaceControl(SurfaceControl surfaceControl) {
733                 mAttrSurfaceControl = surfaceControl;
734                 return this;
735             }
736 
737             /**
738              * Indicates if the instrument should be deferred to the next frame.
739              * @param defer true if the instrument should be deferred to the next frame.
740              * @return builder
741              */
setDeferMonitorForAnimationStart(boolean defer)742             public Builder setDeferMonitorForAnimationStart(boolean defer) {
743                 mAttrDeferMonitor = defer;
744                 return this;
745             }
746 
747             /**
748              * Builds the {@link Configuration} instance
749              * @return the instance of {@link Configuration}
750              * @throws IllegalArgumentException if any invalid attribute is set
751              */
build()752             public Configuration build() throws IllegalArgumentException {
753                 return new Configuration(
754                         mAttrCujType, mAttrView, mAttrTag, mAttrTimeout,
755                         mAttrSurfaceOnly, mAttrContext, mAttrSurfaceControl,
756                         mAttrDeferMonitor);
757             }
758         }
759 
Configuration(@uj.CujType int cuj, View view, @NonNull String tag, long timeout, boolean surfaceOnly, Context context, SurfaceControl surfaceControl, boolean deferMonitor)760         private Configuration(@Cuj.CujType int cuj, View view, @NonNull String tag, long timeout,
761                 boolean surfaceOnly, Context context, SurfaceControl surfaceControl,
762                 boolean deferMonitor) {
763             mCujType = cuj;
764             mTag = tag;
765             mSessionName = generateSessionName(Cuj.getNameOfCuj(cuj), tag);
766             mTimeout = timeout;
767             mView = view;
768             mSurfaceOnly = surfaceOnly;
769             mContext = context != null
770                     ? context
771                     : (view != null ? view.getContext().getApplicationContext() : null);
772             mSurfaceControl = surfaceControl;
773             mDeferMonitor = deferMonitor;
774             validate();
775             mHandler = mSurfaceOnly ? mContext.getMainThreadHandler() : mView.getHandler();
776         }
777 
778         @VisibleForTesting
generateSessionName( @onNull String cujName, @NonNull String cujPostfix)779         public static String generateSessionName(
780                 @NonNull String cujName, @NonNull String cujPostfix) {
781             final boolean hasPostfix = !TextUtils.isEmpty(cujPostfix);
782             if (hasPostfix) {
783                 final int remaining = MAX_LENGTH_SESSION_NAME - cujName.length();
784                 if (cujPostfix.length() > remaining) {
785                     cujPostfix = cujPostfix.substring(0, remaining - 3).concat("...");
786                 }
787             }
788             // The max length of the whole string should be:
789             // 105 with postfix, 83 without postfix
790             return hasPostfix
791                     ? TextUtils.formatSimple("J<%s::%s>", cujName, cujPostfix)
792                     : TextUtils.formatSimple("J<%s>", cujName);
793         }
794 
validate()795         private void validate() {
796             boolean shouldThrow = false;
797             final StringBuilder msg = new StringBuilder();
798 
799             if (mTag == null) {
800                 shouldThrow = true;
801                 msg.append("Invalid tag; ");
802             }
803             if (mTimeout < 0) {
804                 shouldThrow = true;
805                 msg.append("Invalid timeout value; ");
806             }
807             if (mSurfaceOnly) {
808                 if (mContext == null) {
809                     shouldThrow = true;
810                     msg.append("Must pass in a context if only instrument surface; ");
811                 }
812                 if (mSurfaceControl == null || !mSurfaceControl.isValid()) {
813                     shouldThrow = true;
814                     msg.append("Must pass in a valid surface control if only instrument surface; ");
815                 }
816             } else {
817                 if (!hasValidView()) {
818                     shouldThrow = true;
819                     boolean attached = false;
820                     boolean hasViewRoot = false;
821                     boolean hasRenderer = false;
822                     if (mView != null) {
823                         attached = mView.isAttachedToWindow();
824                         hasViewRoot = mView.getViewRootImpl() != null;
825                         hasRenderer = mView.getThreadedRenderer() != null;
826                     }
827                     String err = "invalid view: view=" + mView + ", attached=" + attached
828                             + ", hasViewRoot=" + hasViewRoot + ", hasRenderer=" + hasRenderer;
829                     msg.append(err);
830                 }
831             }
832             if (shouldThrow) {
833                 throw new IllegalArgumentException(msg.toString());
834             }
835         }
836 
hasValidView()837         boolean hasValidView() {
838             return mSurfaceOnly
839                     || (mView != null && mView.isAttachedToWindow()
840                     && mView.getViewRootImpl() != null && mView.getThreadedRenderer() != null);
841         }
842 
843         /**
844          * @return true if only instrumenting surface, false otherwise
845          */
isSurfaceOnly()846         public boolean isSurfaceOnly() {
847             return mSurfaceOnly;
848         }
849 
850         /**
851          * @return the surafce control which is instrumenting
852          */
getSurfaceControl()853         public SurfaceControl getSurfaceControl() {
854             return mSurfaceControl;
855         }
856 
857         /**
858          * @return a view which is attached to the view tree.
859          */
860         @VisibleForTesting
getView()861         public View getView() {
862             return mView;
863         }
864 
865         /**
866          * @return true if the monitoring should be deferred to the next frame, false otherwise.
867          */
shouldDeferMonitor()868         public boolean shouldDeferMonitor() {
869             return mDeferMonitor;
870         }
871 
872         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
getHandler()873         public Handler getHandler() {
874             return mHandler;
875         }
876 
877         /**
878          * @return the ID of the display this interaction in on.
879          */
880         @VisibleForTesting
getDisplayId()881         public int getDisplayId() {
882             return (mSurfaceOnly ? mContext : mView.getContext()).getDisplayId();
883         }
884 
getSessionName()885         public String getSessionName() {
886             return mSessionName;
887         }
888 
getStatsdInteractionType()889         public int getStatsdInteractionType() {
890             return Cuj.getStatsdInteractionType(mCujType);
891         }
892 
893         /** Describes whether the measurement from this session should be written to statsd. */
logToStatsd()894         public boolean logToStatsd() {
895             return Cuj.logToStatsd(mCujType);
896         }
897 
getPerfettoTrigger()898         public String getPerfettoTrigger() {
899             return TextUtils.formatSimple(
900                     "com.android.telemetry.interaction-jank-monitor-%d", mCujType);
901         }
902 
getCujType()903         public @Cuj.CujType int getCujType() {
904             return mCujType;
905         }
906     }
907 
908     @FunctionalInterface
909     private interface TimeFunction {
invoke(long unixNanos, long elapsedNanos, long realtimeNanos)910         void invoke(long unixNanos, long elapsedNanos, long realtimeNanos);
911     }
912 
913     static class RunningTracker {
914         public final Configuration mConfig;
915         public final FrameTracker mTracker;
916         public final Runnable mTimeoutAction;
917 
RunningTracker(Configuration config, FrameTracker tracker, Runnable timeoutAction)918         RunningTracker(Configuration config, FrameTracker tracker, Runnable timeoutAction) {
919             this.mConfig = config;
920             this.mTracker = tracker;
921             this.mTimeoutAction = timeoutAction;
922         }
923     }
924 }
925