blob: 14a6d5e4d08cbba64e53489b37db0dd5c7ff23fe [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.internal.util;
import static android.os.Trace.TRACE_TAG_APP;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Build;
import android.os.SystemClock;
import android.os.Trace;
import android.provider.DeviceConfig;
import android.text.TextUtils;
import android.util.EventLog;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.EventLogTags;
import com.android.internal.os.BackgroundThread;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* Class to track various latencies in SystemUI. It then writes the latency to statsd and also
* outputs it to logcat so these latencies can be captured by tests and then used for dashboards.
* <p>
* This is currently only in Keyguard so it can be shared between SystemUI and Keyguard, but
* eventually we'd want to merge these two packages together so Keyguard can use common classes
* that are shared with SystemUI.
*/
public class LatencyTracker {
private static final String TAG = "LatencyTracker";
public static final String SETTINGS_ENABLED_KEY = "enabled";
private static final String SETTINGS_SAMPLING_INTERVAL_KEY = "sampling_interval";
private static final boolean DEBUG = false;
/** Default to being enabled on debug builds. */
private static final boolean DEFAULT_ENABLED = Build.IS_DEBUGGABLE;
/** Default to collecting data for 1/5 of all actions (randomly sampled). */
private static final int DEFAULT_SAMPLING_INTERVAL = 5;
/**
* Time it takes until the first frame of the notification panel to be displayed while expanding
*/
public static final int ACTION_EXPAND_PANEL = 0;
/**
* Time it takes until the first frame of recents is drawn after invoking it with the button.
*/
public static final int ACTION_TOGGLE_RECENTS = 1;
/**
* Time between we get a fingerprint acquired signal until we start with the unlock animation
*/
public static final int ACTION_FINGERPRINT_WAKE_AND_UNLOCK = 2;
/**
* Time it takes to check PIN/Pattern/Password.
*/
public static final int ACTION_CHECK_CREDENTIAL = 3;
/**
* Time it takes to check fully PIN/Pattern/Password, i.e. that's the time spent including the
* actions to unlock a user.
*/
public static final int ACTION_CHECK_CREDENTIAL_UNLOCKED = 4;
/**
* Time it takes to turn on the screen.
*/
public static final int ACTION_TURN_ON_SCREEN = 5;
/**
* Time it takes to rotate the screen.
*/
public static final int ACTION_ROTATE_SCREEN = 6;
/*
* Time between we get a face acquired signal until we start with the unlock animation
*/
public static final int ACTION_FACE_WAKE_AND_UNLOCK = 7;
/**
* Time between the swipe-up gesture and window drawn of recents activity.
*/
public static final int ACTION_START_RECENTS_ANIMATION = 8;
/**
* Time it takes the sensor to detect rotation.
*/
public static final int ACTION_ROTATE_SCREEN_SENSOR = 9;
/**
* Time it takes to for the camera based algorithm to rotate the screen.
*/
public static final int ACTION_ROTATE_SCREEN_CAMERA_CHECK = 10;
/**
* Time it takes to start unlock animation .
*/
public static final int ACTION_LOCKSCREEN_UNLOCK = 11;
/**
* Time it takes to switch users.
*/
public static final int ACTION_USER_SWITCH = 12;
/**
* Time it takes to turn on the inner screen for a foldable device.
*/
public static final int ACTION_SWITCH_DISPLAY_UNFOLD = 13;
/**
* Time it takes for a UDFPS sensor to appear ready after it is touched.
*/
public static final int ACTION_UDFPS_ILLUMINATE = 14;
/**
* Time it takes for the gesture back affordance arrow to show up.
*/
public static final int ACTION_SHOW_BACK_ARROW = 15;
/**
* Time it takes for loading share sheet.
*/
public static final int ACTION_LOAD_SHARE_SHEET = 16;
/**
* Time it takes to show AOD display after folding the device.
*/
public static final int ACTION_FOLD_TO_AOD = 17;
private static final int[] ACTIONS_ALL = {
ACTION_EXPAND_PANEL,
ACTION_TOGGLE_RECENTS,
ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
ACTION_CHECK_CREDENTIAL,
ACTION_CHECK_CREDENTIAL_UNLOCKED,
ACTION_TURN_ON_SCREEN,
ACTION_ROTATE_SCREEN,
ACTION_FACE_WAKE_AND_UNLOCK,
ACTION_START_RECENTS_ANIMATION,
ACTION_ROTATE_SCREEN_SENSOR,
ACTION_ROTATE_SCREEN_CAMERA_CHECK,
ACTION_LOCKSCREEN_UNLOCK,
ACTION_USER_SWITCH,
ACTION_SWITCH_DISPLAY_UNFOLD,
ACTION_UDFPS_ILLUMINATE,
ACTION_SHOW_BACK_ARROW,
ACTION_LOAD_SHARE_SHEET,
ACTION_FOLD_TO_AOD,
};
/** @hide */
@IntDef({
ACTION_EXPAND_PANEL,
ACTION_TOGGLE_RECENTS,
ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
ACTION_CHECK_CREDENTIAL,
ACTION_CHECK_CREDENTIAL_UNLOCKED,
ACTION_TURN_ON_SCREEN,
ACTION_ROTATE_SCREEN,
ACTION_FACE_WAKE_AND_UNLOCK,
ACTION_START_RECENTS_ANIMATION,
ACTION_ROTATE_SCREEN_SENSOR,
ACTION_ROTATE_SCREEN_CAMERA_CHECK,
ACTION_LOCKSCREEN_UNLOCK,
ACTION_USER_SWITCH,
ACTION_SWITCH_DISPLAY_UNFOLD,
ACTION_UDFPS_ILLUMINATE,
ACTION_SHOW_BACK_ARROW,
ACTION_LOAD_SHARE_SHEET,
ACTION_FOLD_TO_AOD
})
@Retention(RetentionPolicy.SOURCE)
public @interface Action {
}
private static final int[] STATSD_ACTION = new int[]{
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_EXPAND_PANEL,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TOGGLE_RECENTS,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FINGERPRINT_WAKE_AND_UNLOCK,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_CHECK_CREDENTIAL_UNLOCKED,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TURN_ON_SCREEN,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FACE_WAKE_AND_UNLOCK,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_CAMERA_CHECK,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOCKSCREEN_UNLOCK,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_USER_SWITCH,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_UDFPS_ILLUMINATE,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_BACK_ARROW,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET,
FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD
};
private static LatencyTracker sLatencyTracker;
private final Object mLock = new Object();
@GuardedBy("mLock")
private final SparseArray<Session> mSessions = new SparseArray<>();
@GuardedBy("mLock")
private final int[] mTraceThresholdPerAction = new int[ACTIONS_ALL.length];
@GuardedBy("mLock")
private int mSamplingInterval;
@GuardedBy("mLock")
private boolean mEnabled;
public static LatencyTracker getInstance(Context context) {
if (sLatencyTracker == null) {
synchronized (LatencyTracker.class) {
if (sLatencyTracker == null) {
sLatencyTracker = new LatencyTracker();
}
}
}
return sLatencyTracker;
}
private LatencyTracker() {
mEnabled = DEFAULT_ENABLED;
mSamplingInterval = DEFAULT_SAMPLING_INTERVAL;
// Post initialization to the background in case we're running on the main thread.
BackgroundThread.getHandler().post(() -> this.updateProperties(
DeviceConfig.getProperties(DeviceConfig.NAMESPACE_LATENCY_TRACKER)));
DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_LATENCY_TRACKER,
BackgroundThread.getExecutor(), this::updateProperties);
}
private void updateProperties(DeviceConfig.Properties properties) {
synchronized (mLock) {
mSamplingInterval = properties.getInt(SETTINGS_SAMPLING_INTERVAL_KEY,
DEFAULT_SAMPLING_INTERVAL);
mEnabled = properties.getBoolean(SETTINGS_ENABLED_KEY, DEFAULT_ENABLED);
for (int action : ACTIONS_ALL) {
mTraceThresholdPerAction[action] =
properties.getInt(getNameOfAction(STATSD_ACTION[action]), -1);
}
}
}
/**
* A helper method to translate action type to name.
*
* @param atomsProtoAction the action type defined in AtomsProto.java
* @return the name of the action
*/
public static String getNameOfAction(int atomsProtoAction) {
// Defined in AtomsProto.java
switch (atomsProtoAction) {
case 0:
return "UNKNOWN";
case 1:
return "ACTION_EXPAND_PANEL";
case 2:
return "ACTION_TOGGLE_RECENTS";
case 3:
return "ACTION_FINGERPRINT_WAKE_AND_UNLOCK";
case 4:
return "ACTION_CHECK_CREDENTIAL";
case 5:
return "ACTION_CHECK_CREDENTIAL_UNLOCKED";
case 6:
return "ACTION_TURN_ON_SCREEN";
case 7:
return "ACTION_ROTATE_SCREEN";
case 8:
return "ACTION_FACE_WAKE_AND_UNLOCK";
case 9:
return "ACTION_START_RECENTS_ANIMATION";
case 10:
return "ACTION_ROTATE_SCREEN_CAMERA_CHECK";
case 11:
return "ACTION_ROTATE_SCREEN_SENSOR";
case 12:
return "ACTION_LOCKSCREEN_UNLOCK";
case 13:
return "ACTION_USER_SWITCH";
case 14:
return "ACTION_SWITCH_DISPLAY_UNFOLD";
case 15:
return "ACTION_UDFPS_ILLUMINATE";
case 16:
return "ACTION_SHOW_BACK_ARROW";
case 17:
return "ACTION_LOAD_SHARE_SHEET";
case 19:
return "ACTION_FOLD_TO_AOD";
default:
throw new IllegalArgumentException("Invalid action");
}
}
private static String getTraceNameOfAction(@Action int action, String tag) {
if (TextUtils.isEmpty(tag)) {
return "L<" + getNameOfAction(STATSD_ACTION[action]) + ">";
} else {
return "L<" + getNameOfAction(STATSD_ACTION[action]) + "::" + tag + ">";
}
}
private static String getTraceTriggerNameForAction(@Action int action) {
return "com.android.telemetry.latency-tracker-" + getNameOfAction(STATSD_ACTION[action]);
}
public static boolean isEnabled(Context ctx) {
return getInstance(ctx).isEnabled();
}
public boolean isEnabled() {
synchronized (mLock) {
return mEnabled;
}
}
/**
* Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
*
* @param action The action to start. One of the ACTION_* values.
*/
public void onActionStart(@Action int action) {
onActionStart(action, null);
}
/**
* Notifies that an action is starting. <s>This needs to be called from the main thread.</s>
*
* @param action The action to start. One of the ACTION_* values.
* @param tag The brief description of the action.
*/
public void onActionStart(@Action int action, String tag) {
synchronized (mLock) {
if (!isEnabled()) {
return;
}
// skip if the action is already instrumenting.
if (mSessions.get(action) != null) {
return;
}
Session session = new Session(action, tag);
session.begin(() -> onActionCancel(action));
mSessions.put(action, session);
if (DEBUG) {
Log.d(TAG, "onActionStart: " + session.name() + ", start=" + session.mStartRtc);
}
}
}
/**
* Notifies that an action has ended. <s>This needs to be called from the main thread.</s>
*
* @param action The action to end. One of the ACTION_* values.
*/
public void onActionEnd(@Action int action) {
synchronized (mLock) {
if (!isEnabled()) {
return;
}
Session session = mSessions.get(action);
if (session == null) {
return;
}
session.end();
mSessions.delete(action);
logAction(action, session.duration());
if (DEBUG) {
Log.d(TAG, "onActionEnd:" + session.name() + ", duration=" + session.duration());
}
}
}
/**
* Notifies that an action has canceled. <s>This needs to be called from the main thread.</s>
*
* @param action The action to cancel. One of the ACTION_* values.
* @hide
*/
public void onActionCancel(@Action int action) {
synchronized (mLock) {
Session session = mSessions.get(action);
if (session == null) {
return;
}
session.cancel();
mSessions.delete(action);
if (DEBUG) {
Log.d(TAG, "onActionCancel: " + session.name());
}
}
}
/**
* Logs an action that has started and ended. This needs to be called from the main thread.
*
* @param action The action to end. One of the ACTION_* values.
* @param duration The duration of the action in ms.
*/
public void logAction(@Action int action, int duration) {
boolean shouldSample;
int traceThreshold;
synchronized (mLock) {
shouldSample = ThreadLocalRandom.current().nextInt() % mSamplingInterval == 0;
traceThreshold = mTraceThresholdPerAction[action];
}
if (traceThreshold > 0 && duration >= traceThreshold) {
PerfettoTrigger.trigger(getTraceTriggerNameForAction(action));
}
logActionDeprecated(action, duration, shouldSample);
}
/**
* Logs an action that has started and ended. This needs to be called from the main thread.
*
* @param action The action to end. One of the ACTION_* values.
* @param duration The duration of the action in ms.
* @param writeToStatsLog Whether to write the measured latency to FrameworkStatsLog.
*/
public static void logActionDeprecated(
@Action int action, int duration, boolean writeToStatsLog) {
Log.i(TAG, getNameOfAction(STATSD_ACTION[action]) + " latency=" + duration);
EventLog.writeEvent(EventLogTags.SYSUI_LATENCY, action, duration);
if (writeToStatsLog) {
FrameworkStatsLog.write(
FrameworkStatsLog.UI_ACTION_LATENCY_REPORTED, STATSD_ACTION[action], duration);
}
}
static class Session {
@Action
private final int mAction;
private final String mTag;
private final String mName;
private Runnable mTimeoutRunnable;
private long mStartRtc = -1;
private long mEndRtc = -1;
Session(@Action int action, @Nullable String tag) {
mAction = action;
mTag = tag;
mName = TextUtils.isEmpty(mTag)
? getNameOfAction(STATSD_ACTION[mAction])
: getNameOfAction(STATSD_ACTION[mAction]) + "::" + mTag;
}
String name() {
return mName;
}
String traceName() {
return getTraceNameOfAction(mAction, mTag);
}
void begin(@NonNull Runnable timeoutAction) {
mStartRtc = SystemClock.elapsedRealtime();
Trace.asyncTraceBegin(TRACE_TAG_APP, traceName(), 0);
// start counting timeout.
mTimeoutRunnable = timeoutAction;
BackgroundThread.getHandler()
.postDelayed(mTimeoutRunnable, TimeUnit.SECONDS.toMillis(15));
}
void end() {
mEndRtc = SystemClock.elapsedRealtime();
Trace.asyncTraceEnd(TRACE_TAG_APP, traceName(), 0);
BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
mTimeoutRunnable = null;
}
void cancel() {
Trace.asyncTraceEnd(TRACE_TAG_APP, traceName(), 0);
BackgroundThread.getHandler().removeCallbacks(mTimeoutRunnable);
mTimeoutRunnable = null;
}
int duration() {
return (int) (mEndRtc - mStartRtc);
}
}
}