1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.internal.util;
16 
17 import static android.os.Trace.TRACE_TAG_APP;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.os.Build;
24 import android.os.SystemClock;
25 import android.os.Trace;
26 import android.provider.DeviceConfig;
27 import android.text.TextUtils;
28 import android.util.EventLog;
29 import android.util.Log;
30 import android.util.SparseArray;
31 
32 import com.android.internal.annotations.GuardedBy;
33 import com.android.internal.logging.EventLogTags;
34 import com.android.internal.os.BackgroundThread;
35 
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.util.concurrent.ThreadLocalRandom;
39 import java.util.concurrent.TimeUnit;
40 
41 /**
42  * Class to track various latencies in SystemUI. It then writes the latency to statsd and also
43  * outputs it to logcat so these latencies can be captured by tests and then used for dashboards.
44  * <p>
45  * This is currently only in Keyguard so it can be shared between SystemUI and Keyguard, but
46  * eventually we'd want to merge these two packages together so Keyguard can use common classes
47  * that are shared with SystemUI.
48  */
49 public class LatencyTracker {
50     private static final String TAG = "LatencyTracker";
51     private static final String SETTINGS_ENABLED_KEY = "enabled";
52     private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
53     private static final boolean DEBUG = false;
54     /** Default to being enabled on debug builds. */
55     private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE;
56     /** Default to collecting data for 1/5 of all actions (randomly sampled). */
57     private static final int DEFAULT_SAMPLING_INTERVAL = 5;
58 
59     /**
60      * Time it takes until the first frame of the notification panel to be displayed while expanding
61      */
62     public static final int ACTION_EXPAND_PANEL = 0;
63 
64     /**
65      * Time it takes until the first frame of recents is drawn after invoking it with the button.
66      */
67     public static final int ACTION_TOGGLE_RECENTS = 1;
68 
69     /**
70      * Time between we get a fingerprint acquired signal until we start with the unlock animation
71      */
72     public static final int ACTION_FINGERPRINT_WAKE_AND_UNLOCK = 2;
73 
74     /**
75      * Time it takes to check PIN/Pattern/Password.
76      */
77     public static final int ACTION_CHECK_CREDENTIAL = 3;
78 
79     /**
80      * Time it takes to check fully PIN/Pattern/Password, i.e. that's the time spent including the
81      * actions to unlock a user.
82      */
83     public static final int ACTION_CHECK_CREDENTIAL_UNLOCKED = 4;
84 
85     /**
86      * Time it takes to turn on the screen.
87      */
88     public static final int ACTION_TURN_ON_SCREEN = 5;
89 
90     /**
91      * Time it takes to rotate the screen.
92      */
93     public static final int ACTION_ROTATE_SCREEN = 6;
94 
95     /*
96      * Time between we get a face acquired signal until we start with the unlock animation
97      */
98     public static final int ACTION_FACE_WAKE_AND_UNLOCK = 7;
99 
100     /**
101      * Time between the swipe-up gesture and window drawn of recents activity.
102      */
103     public static final int ACTION_START_RECENTS_ANIMATION = 8;
104 
105     /**
106      * Time it takes the sensor to detect rotation.
107      */
108     public static final int ACTION_ROTATE_SCREEN_SENSOR = 9;
109 
110     /**
111      * Time it takes to for the camera based algorithm to rotate the screen.
112      */
113     public static final int ACTION_ROTATE_SCREEN_CAMERA_CHECK = 10;
114 
115     /**
116      * Time it takes to start unlock animation .
117      */
118     public static final int ACTION_LOCKSCREEN_UNLOCK = 11;
119 
120     /**
121      * Time it takes to switch users.
122      */
123     public static final int ACTION_USER_SWITCH = 12;
124 
125     private static final int[] ACTIONS_ALL = {
126         ACTION_EXPAND_PANEL,
127         ACTION_TOGGLE_RECENTS,
128         ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
129         ACTION_CHECK_CREDENTIAL,
130         ACTION_CHECK_CREDENTIAL_UNLOCKED,
131         ACTION_TURN_ON_SCREEN,
132         ACTION_ROTATE_SCREEN,
133         ACTION_FACE_WAKE_AND_UNLOCK,
134         ACTION_START_RECENTS_ANIMATION,
135         ACTION_ROTATE_SCREEN_SENSOR,
136         ACTION_ROTATE_SCREEN_CAMERA_CHECK,
137         ACTION_LOCKSCREEN_UNLOCK,
138         ACTION_USER_SWITCH
139     };
140 
141     /** @hide */
142     @IntDef({
143         ACTION_EXPAND_PANEL,
144         ACTION_TOGGLE_RECENTS,
145         ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
146         ACTION_CHECK_CREDENTIAL,
147         ACTION_CHECK_CREDENTIAL_UNLOCKED,
148         ACTION_TURN_ON_SCREEN,
149         ACTION_ROTATE_SCREEN,
150         ACTION_FACE_WAKE_AND_UNLOCK,
151         ACTION_START_RECENTS_ANIMATION,
152         ACTION_ROTATE_SCREEN_SENSOR,
153         ACTION_ROTATE_SCREEN_CAMERA_CHECK,
154         ACTION_LOCKSCREEN_UNLOCK,
155         ACTION_USER_SWITCH
156     })
157     @Retention(RetentionPolicy.SOURCE)
158     public @interface Action {
159     }
160 
161     private static final int[] STATSD_ACTION = new int[]{
162             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_EXPAND_PANEL,
163             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TOGGLE_RECENTS,
164             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
165             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL,
166             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL_UNLOCKED,
167             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TURN_ON_SCREEN,
168             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN,
169             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK,
170             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION,
171             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR,
172             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_CAMERA_CHECK,
173             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK,
174             FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_USER_SWITCH
175     };
176 
177     private static LatencyTracker sLatencyTracker;
178 
179     private final Object mLock = new Object();
180     @GuardedBy("mLock")
181     private final SparseArray<Session> mSessions = new SparseArray<>();
182     @GuardedBy("mLock")
183     private final int[] mTraceThresholdPerAction = new int[ACTIONS_ALL.length];
184     @GuardedBy("mLock")
185     private int mSamplingInterval;
186     @GuardedBy("mLock")
187     private boolean mEnabled;
188 
getInstance(Context context)189     public static LatencyTracker getInstance(Context context) {
190         if (sLatencyTracker == null) {
191             synchronized (LatencyTracker.class) {
192                 if (sLatencyTracker == null) {
193                     sLatencyTracker = new LatencyTracker();
194                 }
195             }
196         }
197         return sLatencyTracker;
198     }
199 
LatencyTracker()200     private LatencyTracker() {
201         mEnabled = DEFAULT_ENABLED;
202         mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
203 
204         // Post initialization to the background in case we're running on the main thread.
205         BackgroundThread.getHandler().post(() -> this.updateProperties(
206                 DeviceConfig.getProperties(DeviceConfig.NAMESPACE_LATENCY_TRACKER)));
207         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_LATENCY_TRACKER,
208                 BackgroundThread.getExecutor(), this::updateProperties);
209     }
210 
updateProperties(DeviceConfig.Properties properties)211     private void updateProperties(DeviceConfig.Properties properties) {
212         synchronized (mLock) {
213             mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
214                     DEFAULT_SAMPLING_INTERVAL);
215             mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
216             for (int action : ACTIONS_ALL) {
217                 mTraceThresholdPerAction[action] =
218                     properties.getInt(getNameOfAction(STATSD_ACTION[action]), -1);
219             }
220         }
221     }
222 
223     /**
224      * A helper method to translate action type to name.
225      *
226      * @param atomsProtoAction the action type defined in AtomsProto.java
227      * @return the name of the action
228      */
getNameOfAction(int atomsProtoAction)229     public static String getNameOfAction(int atomsProtoAction) {
230         // Defined in AtomsProto.java
231         switch (atomsProtoAction) {
232             case 0:
233                 return "UNKNOWN";
234             case 1:
235                 return "ACTION_EXPAND_PANEL";
236             case 2:
237                 return "ACTION_TOGGLE_RECENTS";
238             case 3:
239                 return "ACTION_FINGERPRINT_WAKE_AND_UNLOCK";
240             case 4:
241                 return "ACTION_CHECK_CREDENTIAL";
242             case 5:
243                 return "ACTION_CHECK_CREDENTIAL_UNLOCKED";
244             case 6:
245                 return "ACTION_TURN_ON_SCREEN";
246             case 7:
247                 return "ACTION_ROTATE_SCREEN";
248             case 8:
249                 return "ACTION_FACE_WAKE_AND_UNLOCK";
250             case 9:
251                 return "ACTION_START_RECENTS_ANIMATION";
252             case 10:
253                 return "ACTION_ROTATE_SCREEN_CAMERA_CHECK";
254             case 11:
255                 return "ACTION_ROTATE_SCREEN_SENSOR";
256             case 12:
257                 return "ACTION_LOCKSCREEN_UNLOCK";
258             case 13:
259                 return "ACTION_USER_SWITCH";
260             default:
261                 throw new IllegalArgumentException("Invalid action");
262         }
263     }
264 
getTraceNameOfAction(@ction int action, String tag)265     private static String getTraceNameOfAction(@Action int action, String tag) {
266         if (TextUtils.isEmpty(tag)) {
267             return "L<" + getNameOfAction(STATSD_ACTION[action]) + ">";
268         } else {
269             return "L<" + getNameOfAction(STATSD_ACTION[action]) + "::" + tag + ">";
270         }
271     }
272 
getTraceTriggerNameForAction(@ction int action)273     private static String getTraceTriggerNameForAction(@Action int action) {
274         return "com.android.telemetry.latency-tracker-" + getNameOfAction(STATSD_ACTION[action]);
275     }
276 
isEnabled(Context ctx)277     public static boolean isEnabled(Context ctx) {
278         return getInstance(ctx).isEnabled();
279     }
280 
isEnabled()281     public boolean isEnabled() {
282         synchronized (mLock) {
283             return mEnabled;
284         }
285     }
286 
287     /**
288      * Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
289      *
290      * @param action The action to start. One of the ACTION_* values.
291      */
onActionStart(@ction int action)292     public void onActionStart(@Action int action) {
293         onActionStart(action, null);
294     }
295 
296     /**
297      * Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
298      *
299      * @param action The action to start. One of the ACTION_* values.
300      * @param tag The brief description of the action.
301      */
onActionStart(@ction int action, String tag)302     public void onActionStart(@Action int action, String tag) {
303         synchronized (mLock) {
304             if (!isEnabled()) {
305                 return;
306             }
307             // skip if the action is already instrumenting.
308             if (mSessions.get(action) != null) {
309                 return;
310             }
311             Session session = new Session(action, tag);
312             session.begin(() -> onActionCancel(action));
313             mSessions.put(action, session);
314 
315             if (DEBUG) {
316                 Log.d(TAG, "onActionStart: " + session.name() + ", start=" + session.mStartRtc);
317             }
318         }
319     }
320 
321     /**
322      * Notifies that an action has ended. <s>This needs to be called from the main thread.</s>
323      *
324      * @param action The action to end. One of the ACTION_* values.
325      */
onActionEnd(@ction int action)326     public void onActionEnd(@Action int action) {
327         synchronized (mLock) {
328             if (!isEnabled()) {
329                 return;
330             }
331             Session session = mSessions.get(action);
332             if (session == null) {
333                 return;
334             }
335             session.end();
336             mSessions.delete(action);
337             logAction(action, session.duration());
338 
339             if (DEBUG) {
340                 Log.d(TAG, "onActionEnd:" + session.name() + ", duration=" + session.duration());
341             }
342         }
343     }
344 
345     /**
346      * Notifies that an action has canceled. <s>This needs to be called from the main thread.</s>
347      *
348      * @param action The action to cancel. One of the ACTION_* values.
349      * @hide
350      */
onActionCancel(@ction int action)351     public void onActionCancel(@Action int action) {
352         synchronized (mLock) {
353             Session session = mSessions.get(action);
354             if (session == null) {
355                 return;
356             }
357             session.cancel();
358             mSessions.delete(action);
359 
360             if (DEBUG) {
361                 Log.d(TAG, "onActionCancel: " + session.name());
362             }
363         }
364     }
365 
366     /**
367      * Logs an action that has started and ended. This needs to be called from the main thread.
368      *
369      * @param action   The action to end. One of the ACTION_* values.
370      * @param duration The duration of the action in ms.
371      */
logAction(@ction int action, int duration)372     public void logAction(@Action int action, int duration) {
373         boolean shouldSample;
374         int traceThreshold;
375         synchronized (mLock) {
376             shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
377             traceThreshold = mTraceThresholdPerAction[action];
378         }
379 
380         if (traceThreshold > 0 && duration >= traceThreshold) {
381             PerfettoTrigger.trigger(getTraceTriggerNameForAction(action));
382         }
383 
384         logActionDeprecated(action, duration, shouldSample);
385     }
386 
387     /**
388      * Logs an action that has started and ended. This needs to be called from the main thread.
389      *
390      * @param action The action to end. One of the ACTION_* values.
391      * @param duration The duration of the action in ms.
392      * @param writeToStatsLog Whether to write the measured latency to FrameworkStatsLog.
393      */
logActionDeprecated( @ction int action, int duration, boolean writeToStatsLog)394     public static void logActionDeprecated(
395             @Action int action, int duration, boolean writeToStatsLog) {
396         Log.i(TAG, getNameOfAction(STATSD_ACTION[action]) + " latency=" + duration);
397         EventLog.writeEvent(EventLogTags.SYSUI_LATENCY, action, duration);
398 
399         if (writeToStatsLog) {
400             FrameworkStatsLog.write(
401                     FrameworkStatsLog.UI_ACTION_LATENCY_REPORTED, STATSD_ACTION[action], duration);
402         }
403     }
404 
405     static class Session {
406         @Action
407         private final int mAction;
408         private final String mTag;
409         private final String mName;
410         private Runnable mTimeoutRunnable;
411         private long mStartRtc = -1;
412         private long mEndRtc = -1;
413 
Session(@ction int action, @Nullable String tag)414         Session(@Action int action, @Nullable String tag) {
415             mAction = action;
416             mTag = tag;
417             mName = TextUtils.isEmpty(mTag)
418                     ? getNameOfAction(STATSD_ACTION[mAction])
419                     : getNameOfAction(STATSD_ACTION[mAction]) + "::" + mTag;
420         }
421 
name()422         String name() {
423             return mName;
424         }
425 
traceName()426         String traceName() {
427             return getTraceNameOfAction(mAction, mTag);
428         }
429 
begin(@onNull Runnable timeoutAction)430         void begin(@NonNull Runnable timeoutAction) {
431             mStartRtc = SystemClock.elapsedRealtime();
432             Trace.asyncTraceBegin(TRACE_TAG_APP, traceName(), 0);
433 
434             // start counting timeout.
435             mTimeoutRunnable = timeoutAction;
436             BackgroundThread.getHandler()
437                     .postDelayed(mTimeoutRunnable, TimeUnit.SECONDS.toMillis(15));
438         }
439 
end()440         void end() {
441             mEndRtc = SystemClock.elapsedRealtime();
442             Trace.asyncTraceEnd(TRACE_TAG_APP, traceName(), 0);
443             BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
444             mTimeoutRunnable = null;
445         }
446 
cancel()447         void cancel() {
448             Trace.asyncTraceEnd(TRACE_TAG_APP, traceName(), 0);
449             BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
450             mTimeoutRunnable = null;
451         }
452 
duration()453         int duration() {
454             return (int) (mEndRtc - mStartRtc);
455         }
456     }
457 }
458