From 55c532525b4a20773c733049cda110fbe8d74568 Mon Sep 17 00:00:00 2001
From: Lee Shombert
Date: Tue, 5 Dec 2023 16:22:44 -0800
Subject: Move AnrTimer to com.android.server.util
Move the new AnrTimer class from services.am to the android server
utility package. This moves code around but changes no functionality
except that the unit test is simplified. The simplification is in
preparation for a complete reworking of the feature code.
Test: atest
* FrameworksServicesTests:com.android.server.am
* FrameworksServicesTests:com.android.server.utils
* FrameworksMockingServicesTests:com.android.server.am
Bug: 282428924
Change-Id: Ic1cdf6832e6ddd04d1bfc0e8883ae3aed9c53262
---
services/core/Android.bp | 1 +
.../java/com/android/server/am/ActiveServices.java | 1 +
.../android/server/am/ActivityManagerService.java | 1 +
.../core/java/com/android/server/am/AnrTimer.java | 1027 --------------------
.../server/am/BroadcastQueueModernImpl.java | 1 +
.../core/java/com/android/server/am/flags.aconfig | 8 -
.../core/java/com/android/server/utils/Android.bp | 10 +
.../java/com/android/server/utils/AnrTimer.java | 1027 ++++++++++++++++++++
.../java/com/android/server/utils/flags.aconfig | 9 +
.../src/com/android/server/am/AnrTimerTest.java | 389 --------
.../src/com/android/server/utils/AnrTimerTest.java | 203 ++++
11 files changed, 1253 insertions(+), 1424 deletions(-)
delete mode 100644 services/core/java/com/android/server/am/AnrTimer.java
create mode 100644 services/core/java/com/android/server/utils/Android.bp
create mode 100644 services/core/java/com/android/server/utils/AnrTimer.java
create mode 100644 services/core/java/com/android/server/utils/flags.aconfig
delete mode 100644 services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java
create mode 100644 services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 3323d0ba64dd..45a582376c26 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -202,6 +202,7 @@ java_library_static {
"biometrics_flags_lib",
"am_flags_lib",
"com_android_wm_shell_flags_lib",
+ "com.android.server.utils_aconfig-java",
"service-jobscheduler-deviceidle.flags-aconfig-java",
],
javac_shard_size: 50,
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 5f1a7e7e8123..71916843fe0b 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -241,6 +241,7 @@ import com.android.server.am.LowMemDetector.MemFactor;
import com.android.server.am.ServiceRecord.ShortFgsInfo;
import com.android.server.pm.KnownPackages;
import com.android.server.uri.NeededUriGrants;
+import com.android.server.utils.AnrTimer;
import com.android.server.wm.ActivityServiceConnectionsHolder;
import java.io.FileDescriptor;
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b87d02d86c22..720e99cd4fcd 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -475,6 +475,7 @@ import com.android.server.sdksandbox.SdkSandboxManagerLocal;
import com.android.server.uri.GrantUri;
import com.android.server.uri.NeededUriGrants;
import com.android.server.uri.UriGrantsManagerInternal;
+import com.android.server.utils.AnrTimer;
import com.android.server.utils.PriorityDump;
import com.android.server.utils.Slogf;
import com.android.server.utils.TimingsTraceAndSlog;
diff --git a/services/core/java/com/android/server/am/AnrTimer.java b/services/core/java/com/android/server/am/AnrTimer.java
deleted file mode 100644
index 3e17930e3cb9..000000000000
--- a/services/core/java/com/android/server/am/AnrTimer.java
+++ /dev/null
@@ -1,1027 +0,0 @@
-/*
- * Copyright (C) 2023 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.server.am;
-
-import static android.text.TextUtils.formatSimple;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.os.SystemClock;
-import android.os.Trace;
-import android.text.TextUtils;
-import android.text.format.TimeMigrationUtils;
-import android.util.ArrayMap;
-import android.util.IndentingPrintWriter;
-import android.util.Log;
-import android.util.MathUtils;
-import android.util.SparseArray;
-
-import com.android.internal.annotations.GuardedBy;
-import com.android.internal.annotations.Keep;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.os.ProcessCpuTracker;
-import com.android.internal.util.RingBuffer;
-
-import java.io.PrintWriter;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Objects;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * This class managers AnrTimers. An AnrTimer is a substitute for a delayed Message. In legacy
- * mode, the timer just sends a delayed message. In modern mode, the timer is implemented in
- * native code; on expiration, the message is sent without delay.
- *
- * There are four external operations on a timer:
- *
- *
- * - {@link #start} starts a timer. The timer is started with an object that the message
- * argument. The timer is also given the pid and uid of the target. A timer that is started must
- * be canceled, accepted, or discarded.
- *
- *
- {@link #cancel} stops a timer and removes any in-flight expiration messages.
- *
- *
- {@link #accept} acknowledges that the timer has expired, and that an ANR should be
- * generated. This clears bookkeeping information for the timer.
- *
- *
- {@link #discard} acknowledges that the timer has expired but, for other reasons, no ANR
- * will be generated. This clears bookkeeping information for the timer.
- *
- *
- *
- * There is one internal operation on a timer: {@link #expire}. A timer may have automatic
- * extensions enabled. If so, the extension is computed and if the extension is non-zero, the timer
- * is restarted with the extension timeout. If extensions are disabled or if the extension is zero,
- * the client process is notified of the expiration.
- *
- * @hide
- */
-class AnrTimer {
-
- /**
- * The log tag.
- */
- final static String TAG = "AnrTimer";
-
- /**
- * The trace track for these events. There is a single track for all AnrTimer instances. The
- * tracks give a sense of handler latency: the time between timer expiration and ANR
- * collection.
- */
- private final static String TRACK = "AnrTimer";
-
- /**
- * Enable debug messages.
- */
- private static boolean DEBUG = false;
-
- /**
- * The trace tag.
- */
- private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
-
- /**
- * Enable tracing from the time a timer expires until it is accepted or discarded. This is
- * used to diagnose long latencies in the client.
- */
- private static final boolean ENABLE_TRACING = false;
-
- /**
- * Return true if the feature is enabled. By default, the value is take from the Flags class
- * but it can be changed for local testing.
- */
- private static boolean anrTimerServiceEnabled() {
- return Flags.anrTimerServiceEnabled();
- }
-
- /**
- * The status of an ANR timer. TIMER_INVALID status is returned when an error is detected.
- */
- private static final int TIMER_INVALID = 0;
- private static final int TIMER_RUNNING = 1;
- private static final int TIMER_EXPIRED = 2;
-
- @IntDef(prefix = { "TIMER_" }, value = {
- TIMER_INVALID, TIMER_RUNNING, TIMER_EXPIRED
- })
- private @interface TimerStatus {}
-
- /**
- * A static list of all known AnrTimer instances, used for dumping and testing.
- */
- @GuardedBy("sAnrTimerList")
- private static final ArrayList> sAnrTimerList = new ArrayList<>();
-
- /**
- * An error is defined by its issue, the operation that detected the error, the tag of the
- * affected service, a short stack of the bad call, and the stringified arg associated with
- * the error.
- */
- private static final class Error {
- /** The issue is the kind of error that was detected. This is a free-form string. */
- final String issue;
- /** The operation that detected the error: start, cancel, accept, or discard. */
- final String operation;
- /** The argument (stringified) passed in to the operation. */
- final String arg;
- /** The tag of the associated AnrTimer. */
- final String tag;
- /** A partial stack that localizes the caller of the operation. */
- final StackTraceElement[] stack;
- /** The date, in local time, the error was created. */
- final long timestamp;
-
- Error(@NonNull String issue, @NonNull String operation, @NonNull String tag,
- @NonNull StackTraceElement[] stack, @NonNull String arg) {
- this.issue = issue;
- this.operation = operation;
- this.tag = tag;
- this.stack = stack;
- this.arg = arg;
- this.timestamp = SystemClock.elapsedRealtime();
- }
- }
-
- /**
- * A list of errors detected during processing. Errors correspond to "timer not found"
- * conditions. The stack trace identifies the source of the call. The list is
- * first-in/first-out, and the size is limited to 20.
- */
- @GuardedBy("sErrors")
- private static final RingBuffer sErrors = new RingBuffer<>(Error.class, 20);
-
- /**
- * A record of a single anr timer. The pid and uid are retained for reference but they do not
- * participate in the equality tests. A {@link Timer} is bound to its parent {@link AnrTimer}
- * through the owner field. Access to timer fields is guarded by the mLock of the owner.
- */
- private static class Timer {
- /** The AnrTimer that is managing this Timer. */
- final AnrTimer owner;
-
- /** The argument that uniquely identifies the Timer in the context of its current owner. */
- final Object arg;
- /** The pid of the process being tracked by this Timer. */
- final int pid;
- /** The uid of the process being tracked by this Timer as reported by the kernel. */
- final int uid;
- /** The original timeout. */
- final long timeoutMs;
-
- /** The status of the Timer. */
- @GuardedBy("owner.mLock")
- @TimerStatus
- int status;
-
- /** The absolute time the timer was startd */
- final long startedMs;
-
- /** Fields used by the native timer service. */
-
- /** The timer ID: used to exchange information with the native service. */
- int timerId;
-
- /** Fields used by the legacy timer service. */
-
- /**
- * The process's cpu delay time when the timer starts . It is meaningful only if
- * extendable is true. The cpu delay is cumulative, so the incremental delay that occurs
- * during a timer is the delay at the end of the timer minus this value. Units are in
- * milliseconds.
- */
- @GuardedBy("owner.mLock")
- long initialCpuDelayMs;
-
- /** True if the timer has been extended. */
- @GuardedBy("owner.mLock")
- boolean extended;
-
- /**
- * Fetch a new Timer. This is private. Clients should get a new timer using the obtain()
- * method.
- */
- private Timer(int pid, int uid, @Nullable Object arg, long timeoutMs,
- @NonNull AnrTimer service) {
- this.arg = arg;
- this.pid = pid;
- this.uid = uid;
- this.timerId = 0;
- this.timeoutMs = timeoutMs;
- this.startedMs = now();
- this.owner = service;
- this.initialCpuDelayMs = 0;
- this.extended = false;
- this.status = TIMER_INVALID;
- }
-
- /** Get a timer. This implementation constructs a new timer. */
- static Timer obtain(int pid, int uid, @Nullable Object arg, long timeout,
- @NonNull AnrTimer service) {
- return new Timer(pid, uid, arg, timeout, service);
- }
-
- /** Release a timer. This implementation simply drops the timer. */
- void release() {
- }
-
- /** Return the age of the timer. This is used for debugging. */
- long age() {
- return now() - startedMs;
- }
-
- /**
- * The hash code is generated from the owner and the argument. By definition, the
- * combination must be unique for the lifetime of an in-use Timer.
- */
- @Override
- public int hashCode() {
- return Objects.hash(owner, arg);
- }
-
- /**
- * The equality check compares the owner and the argument. By definition, the combination
- * must be unique for the lifetime of an in-use Timer.
- */
- @Override
- public boolean equals(Object r) {
- if (r instanceof Timer) {
- Timer t = (Timer) r;
- return Objects.equals(owner, t.owner) && Objects.equals(arg, t.arg);
- }
- return false;
- }
-
- @Override
- public String toString() {
- final int myStatus;
- synchronized (owner.mLock) {
- myStatus = status;
- }
- return "timerId=" + timerId + " pid=" + pid + " uid=" + uid
- + " " + statusString(myStatus) + " " + owner.mLabel;
- }
- }
-
- /** A lock for the AnrTimer instance. */
- private final Object mLock = new Object();
-
- /**
- * The map from client argument to the associated timer.
- */
- @GuardedBy("mLock")
- private final ArrayMap mTimerMap = new ArrayMap<>();
-
- /** The highwater mark of started, but not closed, timers. */
- @GuardedBy("mLock")
- private int mMaxStarted = 0;
-
- /**
- * The total number of timers started.
- */
- @GuardedBy("mLock")
- private int mTotalStarted = 0;
-
- /**
- * The total number of errors detected.
- */
- @GuardedBy("mLock")
- private int mTotalErrors = 0;
-
- /**
- * The total number of timers that have expired.
- */
- @GuardedBy("mLock")
- private int mTotalExpired = 0;
-
- /**
- * A TimerService that generates a timeout event milliseconds in the future. See the
- * class documentation for an explanation of the operations.
- */
- private abstract class TimerService {
- /** Start a timer. The timeout must be initialized. */
- abstract boolean start(@NonNull Timer timer);
-
- abstract void cancel(@NonNull Timer timer);
-
- abstract void accept(@NonNull Timer timer);
-
- abstract void discard(@NonNull Timer timer);
- }
-
- /**
- * A class to assist testing. All methods are null by default but can be overridden as
- * necessary for a test.
- */
- @VisibleForTesting
- static class Injector {
- private final Handler mReferenceHandler;
-
- Injector(@NonNull Handler handler) {
- mReferenceHandler = handler;
- }
-
- /**
- * Return a handler for the given Callback, based on the reference handler. The handler
- * might be mocked, in which case it does not have a valid Looper. In this case, use the
- * main Looper.
- */
- @NonNull
- Handler newHandler(@NonNull Handler.Callback callback) {
- Looper looper = mReferenceHandler.getLooper();
- if (looper == null) looper = Looper.getMainLooper();
- return new Handler(looper, callback);
- }
-
- /**
- * Return a CpuTracker. The default behavior is to create a new CpuTracker but this changes
- * for unit tests.
- **/
- @NonNull
- CpuTracker newTracker() {
- return new CpuTracker();
- }
-
- /** Return true if the feature is enabled. */
- boolean isFeatureEnabled() {
- return anrTimerServiceEnabled();
- }
- }
-
- /**
- * A helper class to measure CPU delays. Given a process ID, this class will return the
- * cumulative CPU delay for the PID, since process inception. This class is defined to assist
- * testing.
- */
- @VisibleForTesting
- static class CpuTracker {
- /**
- * The parameter to ProcessCpuTracker indicates that statistics should be collected on a
- * single process and not on the collection of threads associated with that process.
- */
- private final ProcessCpuTracker mCpu = new ProcessCpuTracker(false);
-
- /** A simple wrapper to fetch the delay. This method can be overridden for testing. */
- long delay(int pid) {
- return mCpu.getCpuDelayTimeForPid(pid);
- }
- }
-
- /**
- * The "user-space" implementation of the timer service. This service uses its own message
- * handler to create timeouts.
- */
- private class HandlerTimerService extends TimerService {
- /** The lock for this handler */
- private final Object mLock = new Object();
-
- /** The message handler for scheduling future events. */
- private final Handler mHandler;
-
- /** The interface to fetch process statistics that might extend an ANR timeout. */
- private final CpuTracker mCpu;
-
- /** Create a HandlerTimerService that directly uses the supplied handler and tracker. */
- @VisibleForTesting
- HandlerTimerService(@NonNull Injector injector) {
- mHandler = injector.newHandler(this::expires);
- mCpu = injector.newTracker();
- }
-
- /** Post a message with the specified timeout. The timer is not modified. */
- private void post(@NonNull Timer t, long timeoutMillis) {
- final Message msg = mHandler.obtainMessage();
- msg.obj = t;
- mHandler.sendMessageDelayed(msg, timeoutMillis);
- }
-
- /**
- * The local expiration handler first attempts to compute a timer extension. If the timer
- * should be extended, it is rescheduled in the future (granting more time to the
- * associated process). If the timer should not be extended then the timeout is delivered
- * to the client.
- *
- * A process is extended to account for the time the process was swapped out and was not
- * runnable through no fault of its own. A timer can only be extended once and only if
- * the AnrTimer permits extensions. Finally, a timer will never be extended by more than
- * the original timeout, so the total timeout will never be more than twice the originally
- * configured timeout.
- */
- private boolean expires(Message msg) {
- Timer t = (Timer) msg.obj;
- synchronized (mLock) {
- long extension = 0;
- if (mExtend && !t.extended) {
- extension = mCpu.delay(t.pid) - t.initialCpuDelayMs;
- if (extension < 0) extension = 0;
- if (extension > t.timeoutMs) extension = t.timeoutMs;
- t.extended = true;
- }
- if (extension > 0) {
- post(t, extension);
- } else {
- onExpiredLocked(t);
- }
- }
- return true;
- }
-
- @GuardedBy("mLock")
- @Override
- boolean start(@NonNull Timer t) {
- if (mExtend) {
- t.initialCpuDelayMs = mCpu.delay(t.pid);
- }
- post(t, t.timeoutMs);
- return true;
- }
-
- @Override
- void cancel(@NonNull Timer t) {
- mHandler.removeMessages(0, t);
- }
-
- @Override
- void accept(@NonNull Timer t) {
- // Nothing to do.
- }
-
- @Override
- void discard(@NonNull Timer t) {
- // Nothing to do.
- }
-
- /** The string identifies this subclass of AnrTimerService as being based on handlers. */
- @Override
- public String toString() {
- return "handler";
- }
- }
-
- /**
- * The handler for messages sent from this instance.
- */
- private final Handler mHandler;
-
- /**
- * The message type for messages sent from this interface.
- */
- private final int mWhat;
-
- /**
- * A label that identifies the AnrTimer associated with a Timer in log messages.
- */
- private final String mLabel;
-
- /**
- * Whether this timer instance supports extending timeouts.
- */
- private final boolean mExtend;
-
- /**
- * The timer service to use for this AnrTimer.
- */
- private final TimerService mTimerService;
-
- /**
- * Whether or not canceling a non-existent timer is an error. Clients often cancel freely
- * preemptively, without knowing if the timer was ever started. Keeping this variable true
- * means that such behavior is not an error.
- */
- private final boolean mLenientCancel = true;
-
- /**
- * The top-level switch for the feature enabled or disabled.
- */
- private final FeatureSwitch mFeature;
-
- /**
- * Create one AnrTimer instance. The instance is given a handler and a "what". Individual
- * timers are started with {@link #start}. If a timer expires, then a {@link Message} is sent
- * immediately to the handler with {@link Message.what} set to what and {@link Message.obj} set
- * to the timer key.
- *
- * AnrTimer instances have a label, which must be unique. The label is used for reporting and
- * debug.
- *
- * If an individual timer expires internally, and the "extend" parameter is true, then the
- * AnrTimer may extend the individual timer rather than immediately delivering the timeout to
- * the client. The extension policy is not part of the instance.
- *
- * This method accepts an {@link #Injector} to tune behavior for testing. This method should
- * not be called directly by regular clients.
- *
- * @param handler The handler to which the expiration message will be delivered.
- * @param what The "what" parameter for the expiration message.
- * @param label A name for this instance.
- * @param extend A flag to indicate if expired timers can be granted extensions.
- * @param injector An {@link #Injector} to tune behavior for testing.
- */
- @VisibleForTesting
- AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend,
- @NonNull Injector injector) {
- mHandler = handler;
- mWhat = what;
- mLabel = label;
- mExtend = extend;
- boolean enabled = injector.isFeatureEnabled();
- if (!enabled) {
- mFeature = new FeatureDisabled();
- mTimerService = null;
- } else {
- mFeature = new FeatureEnabled();
- mTimerService = new HandlerTimerService(injector);
-
- synchronized (sAnrTimerList) {
- sAnrTimerList.add(new WeakReference(this));
- }
- }
- Log.i(TAG, formatSimple("created %s label: \"%s\"", mTimerService, label));
- }
-
- /**
- * Create an AnrTimer instance with the default {@link #Injector}. See {@link AnrTimer(Handler,
- * int, String, boolean, Injector} for a functional description.
- *
- * @param handler The handler to which the expiration message will be delivered.
- * @param what The "what" parameter for the expiration message.
- * @param label A name for this instance.
- * @param extend A flag to indicate if expired timers can be granted extensions.
- */
- AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) {
- this(handler, what, label, extend, new Injector(handler));
- }
-
- /**
- * Create an AnrTimer instance with the default {@link #Injector} and with extensions disabled.
- * See {@link AnrTimer(Handler, int, String, boolean, Injector} for a functional description.
- *
- * @param handler The handler to which the expiration message will be delivered.
- * @param what The "what" parameter for the expiration message.
- * @param label A name for this instance.
- */
- AnrTimer(@NonNull Handler handler, int what, @NonNull String label) {
- this(handler, what, label, false);
- }
-
- /**
- * Return true if the service is enabled on this instance. Clients should use this method to
- * decide if the feature is enabled, and not read the flags directly. This method should be
- * deleted if and when the feature is enabled permanently.
- *
- * @return true if the service is flag-enabled.
- */
- boolean serviceEnabled() {
- return mFeature.enabled();
- }
-
- /**
- * Start a trace on the timer. The trace is laid down in the AnrTimerTrack.
- */
- private void traceBegin(Timer t, String what) {
- if (ENABLE_TRACING) {
- final String label = formatSimple("%s(%d,%d,%s)", what, t.pid, t.uid, mLabel);
- final int cookie = t.hashCode();
- Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie);
- }
- }
-
- /**
- * End a trace on the timer.
- */
- private void traceEnd(Timer t) {
- if (ENABLE_TRACING) {
- final int cookie = t.hashCode();
- Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie);
- }
- }
-
- /**
- * Return the string representation for a timer status.
- */
- private static String statusString(int s) {
- switch (s) {
- case TIMER_INVALID: return "invalid";
- case TIMER_RUNNING: return "running";
- case TIMER_EXPIRED: return "expired";
- }
- return formatSimple("unknown: %d", s);
- }
-
- /**
- * Delete the timer associated with arg from the maps and return it. Return null if the timer
- * was not found.
- */
- @GuardedBy("mLock")
- private Timer removeLocked(V arg) {
- Timer timer = mTimerMap.remove(arg);
- return timer;
- }
-
- /**
- * Return the number of timers currently running.
- */
- @VisibleForTesting
- static int sizeOfTimerList() {
- synchronized (sAnrTimerList) {
- int totalTimers = 0;
- for (int i = 0; i < sAnrTimerList.size(); i++) {
- AnrTimer client = sAnrTimerList.get(i).get();
- if (client != null) totalTimers += client.mTimerMap.size();
- }
- return totalTimers;
- }
- }
-
- /**
- * Clear out all existing timers. This will lead to unexpected behavior if used carelessly.
- * It is available only for testing. It returns the number of times that were actually
- * erased.
- */
- @VisibleForTesting
- static int resetTimerListForHermeticTest() {
- synchronized (sAnrTimerList) {
- int mapLen = 0;
- for (int i = 0; i < sAnrTimerList.size(); i++) {
- AnrTimer client = sAnrTimerList.get(i).get();
- if (client != null) {
- mapLen += client.mTimerMap.size();
- client.mTimerMap.clear();
- }
- }
- if (mapLen > 0) {
- Log.w(TAG, formatSimple("erasing timer list: clearing %d timers", mapLen));
- }
- return mapLen;
- }
- }
-
- /**
- * Generate a log message for a timer.
- */
- private void report(@NonNull Timer timer, @NonNull String msg) {
- Log.i(TAG, msg + " " + timer + " " + Objects.toString(timer.arg));
- }
-
- /**
- * The FeatureSwitch class provides a quick switch between feature-enabled behavior and
- * feature-disabled behavior.
- */
- private abstract class FeatureSwitch {
- abstract boolean start(@NonNull V arg, int pid, int uid, long timeoutMs);
-
- abstract boolean cancel(@NonNull V arg);
-
- abstract boolean accept(@NonNull V arg);
-
- abstract boolean discard(@NonNull V arg);
-
- abstract boolean enabled();
- }
-
- /**
- * The FeatureDisabled class bypasses almost all AnrTimer logic. It is used when the AnrTimer
- * service is disabled via Flags.anrTimerServiceEnabled.
- */
- private class FeatureDisabled extends FeatureSwitch {
- /** Start a timer by sending a message to the client's handler. */
- @Override
- boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
- final Message msg = mHandler.obtainMessage(mWhat, arg);
- mHandler.sendMessageDelayed(msg, timeoutMs);
- return true;
- }
-
- /** Cancel a timer by removing the message from the client's handler. */
- @Override
- boolean cancel(@NonNull V arg) {
- mHandler.removeMessages(mWhat, arg);
- return true;
- }
-
- /** accept() is a no-op when the feature is disabled. */
- @Override
- boolean accept(@NonNull V arg) {
- return true;
- }
-
- /** discard() is a no-op when the feature is disabled. */
- @Override
- boolean discard(@NonNull V arg) {
- return true;
- }
-
- /** The feature is not enabled. */
- @Override
- boolean enabled() {
- return false;
- }
- }
-
- /**
- * The FeatureEnabled class enables the AnrTimer logic. It is used when the AnrTimer service
- * is enabled via Flags.anrTimerServiceEnabled.
- */
- private class FeatureEnabled extends FeatureSwitch {
-
- /**
- * Start a timer.
- */
- @Override
- boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
- final Timer timer = Timer.obtain(pid, uid, arg, timeoutMs, AnrTimer.this);
- synchronized (mLock) {
- Timer old = mTimerMap.get(arg);
- // There is an existing timer. If the timer was running, then cancel the running
- // timer and restart it. If the timer was expired record a protocol error and
- // discard the expired timer.
- if (old != null) {
- if (old.status == TIMER_EXPIRED) {
- restartedLocked(old.status, arg);
- discard(arg);
- } else {
- cancel(arg);
- }
- }
- if (mTimerService.start(timer)) {
- timer.status = TIMER_RUNNING;
- mTimerMap.put(arg, timer);
- mTotalStarted++;
- mMaxStarted = Math.max(mMaxStarted, mTimerMap.size());
- if (DEBUG) report(timer, "start");
- return true;
- } else {
- Log.e(TAG, "AnrTimer.start failed");
- return false;
- }
- }
- }
-
- /**
- * Cancel a timer. Return false if the timer was not found.
- */
- @Override
- boolean cancel(@NonNull V arg) {
- synchronized (mLock) {
- Timer timer = removeLocked(arg);
- if (timer == null) {
- if (!mLenientCancel) notFoundLocked("cancel", arg);
- return false;
- }
- mTimerService.cancel(timer);
- // There may be an expiration message in flight. Cancel it.
- mHandler.removeMessages(mWhat, arg);
- if (DEBUG) report(timer, "cancel");
- timer.release();
- return true;
- }
- }
-
- /**
- * Accept a timer in the framework-level handler. The timeout has been accepted and the
- * timeout handler is executing. Return false if the timer was not found.
- */
- @Override
- boolean accept(@NonNull V arg) {
- synchronized (mLock) {
- Timer timer = removeLocked(arg);
- if (timer == null) {
- notFoundLocked("accept", arg);
- return false;
- }
- mTimerService.accept(timer);
- traceEnd(timer);
- if (DEBUG) report(timer, "accept");
- timer.release();
- return true;
- }
- }
-
- /**
- * Discard a timer in the framework-level handler. For whatever reason, the timer is no
- * longer interesting. No statistics are collected. Return false if the time was not
- * found.
- */
- @Override
- boolean discard(@NonNull V arg) {
- synchronized (mLock) {
- Timer timer = removeLocked(arg);
- if (timer == null) {
- notFoundLocked("discard", arg);
- return false;
- }
- mTimerService.discard(timer);
- traceEnd(timer);
- if (DEBUG) report(timer, "discard");
- timer.release();
- return true;
- }
- }
-
- /** The feature is enabled. */
- @Override
- boolean enabled() {
- return true;
- }
- }
-
- /**
- * Start a timer associated with arg. The same object must be used to cancel, accept, or
- * discard a timer later. If a timer already exists with the same arg, then the existing timer
- * is canceled and a new timer is created.
- *
- * @param arg The key by which the timer is known. This is never examined or modified.
- * @param pid The Linux process ID of the target being timed.
- * @param uid The Linux user ID of the target being timed.
- * @param timeoutMs The timer timeout, in milliseconds.
- * @return true if the timer was successfully created.
- */
- boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
- return mFeature.start(arg, pid, uid, timeoutMs);
- }
-
- /**
- * Cancel the running timer associated with arg. The timer is forgotten. If the timer has
- * expired, the call is treated as a discard. No errors are reported if the timer does not
- * exist or if the timer has expired.
- *
- * @return true if the timer was found and was running.
- */
- boolean cancel(@NonNull V arg) {
- return mFeature.cancel(arg);
- }
-
- /**
- * Accept the expired timer associated with arg. This indicates that the caller considers the
- * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) It is
- * an error to accept a running timer, however the running timer will be canceled.
- *
- * @return true if the timer was found and was expired.
- */
- boolean accept(@NonNull V arg) {
- return mFeature.accept(arg);
- }
-
- /**
- * Discard the expired timer associated with arg. This indicates that the caller considers the
- * timer expiration to be a false ANR. ((See {@link #accept} for an alternate response.) One
- * reason to discard an expired timer is if the process being timed was also being debugged:
- * such a process could be stopped at a breakpoint and its failure to respond would not be an
- * error. It is an error to discard a running timer, however the running timer will be
- * canceled.
- *
- * @return true if the timer was found and was expired.
- */
- boolean discard(@NonNull V arg) {
- return mFeature.discard(arg);
- }
-
- /**
- * The notifier that a timer has fired. The timer is not modified.
- */
- @GuardedBy("mLock")
- private void onExpiredLocked(@NonNull Timer timer) {
- if (DEBUG) report(timer, "expire");
- traceBegin(timer, "expired");
- mHandler.sendMessage(Message.obtain(mHandler, mWhat, timer.arg));
- synchronized (mLock) {
- mTotalExpired++;
- }
- }
-
- /**
- * Dump a single AnrTimer.
- */
- private void dump(IndentingPrintWriter pw) {
- synchronized (mLock) {
- pw.format("timer: %s\n", mLabel);
- pw.increaseIndent();
- pw.format("started=%d maxStarted=%d running=%d expired=%d error=%d\n",
- mTotalStarted, mMaxStarted, mTimerMap.size(),
- mTotalExpired, mTotalErrors);
- pw.decreaseIndent();
- }
- }
-
- /**
- * Enable or disable debugging.
- */
- static void debug(boolean f) {
- DEBUG = f;
- }
-
- /**
- * The current time in milliseconds.
- */
- private static long now() {
- return SystemClock.uptimeMillis();
- }
-
- /**
- * Log an error. A limited stack trace leading to the client call that triggered the error is
- * recorded. The stack trace assumes that this method is not called directly.
- *
- * If DEBUG is true, a log message is generated as well.
- */
- @GuardedBy("mLock")
- private void recordErrorLocked(String operation, String errorMsg, Object arg) {
- StackTraceElement[] s = Thread.currentThread().getStackTrace();
- final String what = Objects.toString(arg);
- // The copy range starts at the caller of the timer operation, and includes three levels.
- // This should be enough to isolate the location of the call.
- StackTraceElement[] location = Arrays.copyOfRange(s, 6, 9);
- synchronized (sErrors) {
- sErrors.append(new Error(errorMsg, operation, mLabel, location, what));
- }
- if (DEBUG) Log.w(TAG, operation + " " + errorMsg + " " + mLabel + " timer " + what);
- mTotalErrors++;
- }
-
- /**
- * Log an error about a timer not found.
- */
- @GuardedBy("mLock")
- private void notFoundLocked(String operation, Object arg) {
- recordErrorLocked(operation, "notFound", arg);
- }
-
- /**
- * Log an error about a timer that is started when there is an existing timer.
- */
- @GuardedBy("mLock")
- private void restartedLocked(@TimerStatus int status, Object arg) {
- recordErrorLocked("start", status == TIMER_EXPIRED ? "autoDiscard" : "autoCancel", arg);
- }
-
- /**
- * Dump a single error to the output stream.
- */
- private static void dump(IndentingPrintWriter ipw, int seq, Error err) {
- ipw.format("%2d: op:%s tag:%s issue:%s arg:%s\n", seq, err.operation, err.tag,
- err.issue, err.arg);
-
- final long offset = System.currentTimeMillis() - SystemClock.elapsedRealtime();
- final long etime = offset + err.timestamp;
- ipw.println(" date:" + TimeMigrationUtils.formatMillisWithFixedFormat(etime));
- ipw.increaseIndent();
- for (int i = 0; i < err.stack.length; i++) {
- ipw.println(" " + err.stack[i].toString());
- }
- ipw.decreaseIndent();
- }
-
- /**
- * Dump all errors to the output stream.
- */
- private static void dumpErrors(IndentingPrintWriter ipw) {
- Error errors[];
- synchronized (sErrors) {
- if (sErrors.size() == 0) return;
- errors = sErrors.toArray();
- }
- ipw.println("Errors");
- ipw.increaseIndent();
- for (int i = 0; i < errors.length; i++) {
- if (errors[i] != null) dump(ipw, i, errors[i]);
- }
- ipw.decreaseIndent();
- }
-
- /**
- * Dumpsys output.
- */
- static void dump(@NonNull PrintWriter pw, boolean verbose) {
- final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
- ipw.println("AnrTimer statistics");
- ipw.increaseIndent();
- synchronized (sAnrTimerList) {
- for (int i = 0; i < sAnrTimerList.size(); i++) {
- AnrTimer client = sAnrTimerList.get(i).get();
- if (client != null) client.dump(ipw);
- }
- }
- if (verbose) dumpErrors(ipw);
- ipw.format("AnrTimerEnd\n");
- ipw.decreaseIndent();
- }
-}
diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
index e07631cfbdb0..7f5ef7ecf57d 100644
--- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
+++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java
@@ -88,6 +88,7 @@ import com.android.internal.util.FrameworkStatsLog;
import com.android.server.am.BroadcastProcessQueue.BroadcastConsumer;
import com.android.server.am.BroadcastProcessQueue.BroadcastPredicate;
import com.android.server.am.BroadcastRecord.DeliveryState;
+import com.android.server.utils.AnrTimer;
import dalvik.annotation.optimization.NeverCompile;
diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig
index a770b66b2506..d0fe69583981 100644
--- a/services/core/java/com/android/server/am/flags.aconfig
+++ b/services/core/java/com/android/server/am/flags.aconfig
@@ -8,14 +8,6 @@ flag {
is_fixed_read_only: true
}
-flag {
- name: "anr_timer_service_enabled"
- namespace: "system_performance"
- is_fixed_read_only: true
- description: "Feature flag for the ANR timer service"
- bug: "282428924"
-}
-
flag {
name: "fgs_abuse_detection"
namespace: "backstage_power"
diff --git a/services/core/java/com/android/server/utils/Android.bp b/services/core/java/com/android/server/utils/Android.bp
new file mode 100644
index 000000000000..3a334bee93ff
--- /dev/null
+++ b/services/core/java/com/android/server/utils/Android.bp
@@ -0,0 +1,10 @@
+aconfig_declarations {
+ name: "com.android.server.utils-aconfig",
+ package: "com.android.server.utils",
+ srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+ name: "com.android.server.utils_aconfig-java",
+ aconfig_declarations: "com.android.server.utils-aconfig",
+}
diff --git a/services/core/java/com/android/server/utils/AnrTimer.java b/services/core/java/com/android/server/utils/AnrTimer.java
new file mode 100644
index 000000000000..2b6dffb2b271
--- /dev/null
+++ b/services/core/java/com/android/server/utils/AnrTimer.java
@@ -0,0 +1,1027 @@
+/*
+ * Copyright (C) 2023 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.server.utils;
+
+import static android.text.TextUtils.formatSimple;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.text.TextUtils;
+import android.text.format.TimeMigrationUtils;
+import android.util.ArrayMap;
+import android.util.IndentingPrintWriter;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Keep;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.ProcessCpuTracker;
+import com.android.internal.util.RingBuffer;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * This class managers AnrTimers. An AnrTimer is a substitute for a delayed Message. In legacy
+ * mode, the timer just sends a delayed message. In modern mode, the timer is implemented in
+ * native code; on expiration, the message is sent without delay.
+ *
+ * There are four external operations on a timer:
+ *
+ *
+ * - {@link #start} starts a timer. The timer is started with an object that the message
+ * argument. The timer is also given the pid and uid of the target. A timer that is started must
+ * be canceled, accepted, or discarded.
+ *
+ *
- {@link #cancel} stops a timer and removes any in-flight expiration messages.
+ *
+ *
- {@link #accept} acknowledges that the timer has expired, and that an ANR should be
+ * generated. This clears bookkeeping information for the timer.
+ *
+ *
- {@link #discard} acknowledges that the timer has expired but, for other reasons, no ANR
+ * will be generated. This clears bookkeeping information for the timer.
+ *
+ *
+ *
+ * There is one internal operation on a timer: {@link #expire}. A timer may have automatic
+ * extensions enabled. If so, the extension is computed and if the extension is non-zero, the timer
+ * is restarted with the extension timeout. If extensions are disabled or if the extension is zero,
+ * the client process is notified of the expiration.
+ *
+ * @hide
+ */
+public class AnrTimer {
+
+ /**
+ * The log tag.
+ */
+ final static String TAG = "AnrTimer";
+
+ /**
+ * The trace track for these events. There is a single track for all AnrTimer instances. The
+ * tracks give a sense of handler latency: the time between timer expiration and ANR
+ * collection.
+ */
+ private final static String TRACK = "AnrTimer";
+
+ /**
+ * Enable debug messages.
+ */
+ private static boolean DEBUG = false;
+
+ /**
+ * The trace tag.
+ */
+ private static final long TRACE_TAG = Trace.TRACE_TAG_ACTIVITY_MANAGER;
+
+ /**
+ * Enable tracing from the time a timer expires until it is accepted or discarded. This is
+ * used to diagnose long latencies in the client.
+ */
+ private static final boolean ENABLE_TRACING = false;
+
+ /**
+ * Return true if the feature is enabled. By default, the value is take from the Flags class
+ * but it can be changed for local testing.
+ */
+ private static boolean anrTimerServiceEnabled() {
+ return Flags.anrTimerServiceEnabled();
+ }
+
+ /**
+ * The status of an ANR timer. TIMER_INVALID status is returned when an error is detected.
+ */
+ private static final int TIMER_INVALID = 0;
+ private static final int TIMER_RUNNING = 1;
+ private static final int TIMER_EXPIRED = 2;
+
+ @IntDef(prefix = { "TIMER_" }, value = {
+ TIMER_INVALID, TIMER_RUNNING, TIMER_EXPIRED
+ })
+ private @interface TimerStatus {}
+
+ /**
+ * A static list of all known AnrTimer instances, used for dumping and testing.
+ */
+ @GuardedBy("sAnrTimerList")
+ private static final ArrayList> sAnrTimerList = new ArrayList<>();
+
+ /**
+ * An error is defined by its issue, the operation that detected the error, the tag of the
+ * affected service, a short stack of the bad call, and the stringified arg associated with
+ * the error.
+ */
+ private static final class Error {
+ /** The issue is the kind of error that was detected. This is a free-form string. */
+ final String issue;
+ /** The operation that detected the error: start, cancel, accept, or discard. */
+ final String operation;
+ /** The argument (stringified) passed in to the operation. */
+ final String arg;
+ /** The tag of the associated AnrTimer. */
+ final String tag;
+ /** A partial stack that localizes the caller of the operation. */
+ final StackTraceElement[] stack;
+ /** The date, in local time, the error was created. */
+ final long timestamp;
+
+ Error(@NonNull String issue, @NonNull String operation, @NonNull String tag,
+ @NonNull StackTraceElement[] stack, @NonNull String arg) {
+ this.issue = issue;
+ this.operation = operation;
+ this.tag = tag;
+ this.stack = stack;
+ this.arg = arg;
+ this.timestamp = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * A list of errors detected during processing. Errors correspond to "timer not found"
+ * conditions. The stack trace identifies the source of the call. The list is
+ * first-in/first-out, and the size is limited to 20.
+ */
+ @GuardedBy("sErrors")
+ private static final RingBuffer sErrors = new RingBuffer<>(Error.class, 20);
+
+ /**
+ * A record of a single anr timer. The pid and uid are retained for reference but they do not
+ * participate in the equality tests. A {@link Timer} is bound to its parent {@link AnrTimer}
+ * through the owner field. Access to timer fields is guarded by the mLock of the owner.
+ */
+ private static class Timer {
+ /** The AnrTimer that is managing this Timer. */
+ final AnrTimer owner;
+
+ /** The argument that uniquely identifies the Timer in the context of its current owner. */
+ final Object arg;
+ /** The pid of the process being tracked by this Timer. */
+ final int pid;
+ /** The uid of the process being tracked by this Timer as reported by the kernel. */
+ final int uid;
+ /** The original timeout. */
+ final long timeoutMs;
+
+ /** The status of the Timer. */
+ @GuardedBy("owner.mLock")
+ @TimerStatus
+ int status;
+
+ /** The absolute time the timer was startd */
+ final long startedMs;
+
+ /** Fields used by the native timer service. */
+
+ /** The timer ID: used to exchange information with the native service. */
+ int timerId;
+
+ /** Fields used by the legacy timer service. */
+
+ /**
+ * The process's cpu delay time when the timer starts . It is meaningful only if
+ * extendable is true. The cpu delay is cumulative, so the incremental delay that occurs
+ * during a timer is the delay at the end of the timer minus this value. Units are in
+ * milliseconds.
+ */
+ @GuardedBy("owner.mLock")
+ long initialCpuDelayMs;
+
+ /** True if the timer has been extended. */
+ @GuardedBy("owner.mLock")
+ boolean extended;
+
+ /**
+ * Fetch a new Timer. This is private. Clients should get a new timer using the obtain()
+ * method.
+ */
+ private Timer(int pid, int uid, @Nullable Object arg, long timeoutMs,
+ @NonNull AnrTimer service) {
+ this.arg = arg;
+ this.pid = pid;
+ this.uid = uid;
+ this.timerId = 0;
+ this.timeoutMs = timeoutMs;
+ this.startedMs = now();
+ this.owner = service;
+ this.initialCpuDelayMs = 0;
+ this.extended = false;
+ this.status = TIMER_INVALID;
+ }
+
+ /** Get a timer. This implementation constructs a new timer. */
+ static Timer obtain(int pid, int uid, @Nullable Object arg, long timeout,
+ @NonNull AnrTimer service) {
+ return new Timer(pid, uid, arg, timeout, service);
+ }
+
+ /** Release a timer. This implementation simply drops the timer. */
+ void release() {
+ }
+
+ /** Return the age of the timer. This is used for debugging. */
+ long age() {
+ return now() - startedMs;
+ }
+
+ /**
+ * The hash code is generated from the owner and the argument. By definition, the
+ * combination must be unique for the lifetime of an in-use Timer.
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(owner, arg);
+ }
+
+ /**
+ * The equality check compares the owner and the argument. By definition, the combination
+ * must be unique for the lifetime of an in-use Timer.
+ */
+ @Override
+ public boolean equals(Object r) {
+ if (r instanceof Timer) {
+ Timer t = (Timer) r;
+ return Objects.equals(owner, t.owner) && Objects.equals(arg, t.arg);
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ final int myStatus;
+ synchronized (owner.mLock) {
+ myStatus = status;
+ }
+ return "timerId=" + timerId + " pid=" + pid + " uid=" + uid
+ + " " + statusString(myStatus) + " " + owner.mLabel;
+ }
+ }
+
+ /** A lock for the AnrTimer instance. */
+ private final Object mLock = new Object();
+
+ /**
+ * The map from client argument to the associated timer.
+ */
+ @GuardedBy("mLock")
+ private final ArrayMap mTimerMap = new ArrayMap<>();
+
+ /** The highwater mark of started, but not closed, timers. */
+ @GuardedBy("mLock")
+ private int mMaxStarted = 0;
+
+ /**
+ * The total number of timers started.
+ */
+ @GuardedBy("mLock")
+ private int mTotalStarted = 0;
+
+ /**
+ * The total number of errors detected.
+ */
+ @GuardedBy("mLock")
+ private int mTotalErrors = 0;
+
+ /**
+ * The total number of timers that have expired.
+ */
+ @GuardedBy("mLock")
+ private int mTotalExpired = 0;
+
+ /**
+ * A TimerService that generates a timeout event milliseconds in the future. See the
+ * class documentation for an explanation of the operations.
+ */
+ private abstract class TimerService {
+ /** Start a timer. The timeout must be initialized. */
+ abstract boolean start(@NonNull Timer timer);
+
+ abstract void cancel(@NonNull Timer timer);
+
+ abstract void accept(@NonNull Timer timer);
+
+ abstract void discard(@NonNull Timer timer);
+ }
+
+ /**
+ * A class to assist testing. All methods are null by default but can be overridden as
+ * necessary for a test.
+ */
+ @VisibleForTesting
+ static class Injector {
+ private final Handler mReferenceHandler;
+
+ Injector(@NonNull Handler handler) {
+ mReferenceHandler = handler;
+ }
+
+ /**
+ * Return a handler for the given Callback, based on the reference handler. The handler
+ * might be mocked, in which case it does not have a valid Looper. In this case, use the
+ * main Looper.
+ */
+ @NonNull
+ Handler newHandler(@NonNull Handler.Callback callback) {
+ Looper looper = mReferenceHandler.getLooper();
+ if (looper == null) looper = Looper.getMainLooper();
+ return new Handler(looper, callback);
+ }
+
+ /**
+ * Return a CpuTracker. The default behavior is to create a new CpuTracker but this changes
+ * for unit tests.
+ **/
+ @NonNull
+ CpuTracker newTracker() {
+ return new CpuTracker();
+ }
+
+ /** Return true if the feature is enabled. */
+ boolean isFeatureEnabled() {
+ return anrTimerServiceEnabled();
+ }
+ }
+
+ /**
+ * A helper class to measure CPU delays. Given a process ID, this class will return the
+ * cumulative CPU delay for the PID, since process inception. This class is defined to assist
+ * testing.
+ */
+ @VisibleForTesting
+ static class CpuTracker {
+ /**
+ * The parameter to ProcessCpuTracker indicates that statistics should be collected on a
+ * single process and not on the collection of threads associated with that process.
+ */
+ private final ProcessCpuTracker mCpu = new ProcessCpuTracker(false);
+
+ /** A simple wrapper to fetch the delay. This method can be overridden for testing. */
+ long delay(int pid) {
+ return mCpu.getCpuDelayTimeForPid(pid);
+ }
+ }
+
+ /**
+ * The "user-space" implementation of the timer service. This service uses its own message
+ * handler to create timeouts.
+ */
+ private class HandlerTimerService extends TimerService {
+ /** The lock for this handler */
+ private final Object mLock = new Object();
+
+ /** The message handler for scheduling future events. */
+ private final Handler mHandler;
+
+ /** The interface to fetch process statistics that might extend an ANR timeout. */
+ private final CpuTracker mCpu;
+
+ /** Create a HandlerTimerService that directly uses the supplied handler and tracker. */
+ @VisibleForTesting
+ HandlerTimerService(@NonNull Injector injector) {
+ mHandler = injector.newHandler(this::expires);
+ mCpu = injector.newTracker();
+ }
+
+ /** Post a message with the specified timeout. The timer is not modified. */
+ private void post(@NonNull Timer t, long timeoutMillis) {
+ final Message msg = mHandler.obtainMessage();
+ msg.obj = t;
+ mHandler.sendMessageDelayed(msg, timeoutMillis);
+ }
+
+ /**
+ * The local expiration handler first attempts to compute a timer extension. If the timer
+ * should be extended, it is rescheduled in the future (granting more time to the
+ * associated process). If the timer should not be extended then the timeout is delivered
+ * to the client.
+ *
+ * A process is extended to account for the time the process was swapped out and was not
+ * runnable through no fault of its own. A timer can only be extended once and only if
+ * the AnrTimer permits extensions. Finally, a timer will never be extended by more than
+ * the original timeout, so the total timeout will never be more than twice the originally
+ * configured timeout.
+ */
+ private boolean expires(Message msg) {
+ Timer t = (Timer) msg.obj;
+ synchronized (mLock) {
+ long extension = 0;
+ if (mExtend && !t.extended) {
+ extension = mCpu.delay(t.pid) - t.initialCpuDelayMs;
+ if (extension < 0) extension = 0;
+ if (extension > t.timeoutMs) extension = t.timeoutMs;
+ t.extended = true;
+ }
+ if (extension > 0) {
+ post(t, extension);
+ } else {
+ onExpiredLocked(t);
+ }
+ }
+ return true;
+ }
+
+ @GuardedBy("mLock")
+ @Override
+ boolean start(@NonNull Timer t) {
+ if (mExtend) {
+ t.initialCpuDelayMs = mCpu.delay(t.pid);
+ }
+ post(t, t.timeoutMs);
+ return true;
+ }
+
+ @Override
+ void cancel(@NonNull Timer t) {
+ mHandler.removeMessages(0, t);
+ }
+
+ @Override
+ void accept(@NonNull Timer t) {
+ // Nothing to do.
+ }
+
+ @Override
+ void discard(@NonNull Timer t) {
+ // Nothing to do.
+ }
+
+ /** The string identifies this subclass of AnrTimerService as being based on handlers. */
+ @Override
+ public String toString() {
+ return "handler";
+ }
+ }
+
+ /**
+ * The handler for messages sent from this instance.
+ */
+ private final Handler mHandler;
+
+ /**
+ * The message type for messages sent from this interface.
+ */
+ private final int mWhat;
+
+ /**
+ * A label that identifies the AnrTimer associated with a Timer in log messages.
+ */
+ private final String mLabel;
+
+ /**
+ * Whether this timer instance supports extending timeouts.
+ */
+ private final boolean mExtend;
+
+ /**
+ * The timer service to use for this AnrTimer.
+ */
+ private final TimerService mTimerService;
+
+ /**
+ * Whether or not canceling a non-existent timer is an error. Clients often cancel freely
+ * preemptively, without knowing if the timer was ever started. Keeping this variable true
+ * means that such behavior is not an error.
+ */
+ private final boolean mLenientCancel = true;
+
+ /**
+ * The top-level switch for the feature enabled or disabled.
+ */
+ private final FeatureSwitch mFeature;
+
+ /**
+ * Create one AnrTimer instance. The instance is given a handler and a "what". Individual
+ * timers are started with {@link #start}. If a timer expires, then a {@link Message} is sent
+ * immediately to the handler with {@link Message.what} set to what and {@link Message.obj} set
+ * to the timer key.
+ *
+ * AnrTimer instances have a label, which must be unique. The label is used for reporting and
+ * debug.
+ *
+ * If an individual timer expires internally, and the "extend" parameter is true, then the
+ * AnrTimer may extend the individual timer rather than immediately delivering the timeout to
+ * the client. The extension policy is not part of the instance.
+ *
+ * This method accepts an {@link #Injector} to tune behavior for testing. This method should
+ * not be called directly by regular clients.
+ *
+ * @param handler The handler to which the expiration message will be delivered.
+ * @param what The "what" parameter for the expiration message.
+ * @param label A name for this instance.
+ * @param extend A flag to indicate if expired timers can be granted extensions.
+ * @param injector An {@link #Injector} to tune behavior for testing.
+ */
+ @VisibleForTesting
+ AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend,
+ @NonNull Injector injector) {
+ mHandler = handler;
+ mWhat = what;
+ mLabel = label;
+ mExtend = extend;
+ boolean enabled = injector.isFeatureEnabled();
+ if (!enabled) {
+ mFeature = new FeatureDisabled();
+ mTimerService = null;
+ } else {
+ mFeature = new FeatureEnabled();
+ mTimerService = new HandlerTimerService(injector);
+
+ synchronized (sAnrTimerList) {
+ sAnrTimerList.add(new WeakReference(this));
+ }
+ }
+ Log.i(TAG, formatSimple("created %s label: \"%s\"", mTimerService, label));
+ }
+
+ /**
+ * Create an AnrTimer instance with the default {@link #Injector}. See {@link AnrTimer(Handler,
+ * int, String, boolean, Injector} for a functional description.
+ *
+ * @param handler The handler to which the expiration message will be delivered.
+ * @param what The "what" parameter for the expiration message.
+ * @param label A name for this instance.
+ * @param extend A flag to indicate if expired timers can be granted extensions.
+ */
+ public AnrTimer(@NonNull Handler handler, int what, @NonNull String label, boolean extend) {
+ this(handler, what, label, extend, new Injector(handler));
+ }
+
+ /**
+ * Create an AnrTimer instance with the default {@link #Injector} and with extensions disabled.
+ * See {@link AnrTimer(Handler, int, String, boolean, Injector} for a functional description.
+ *
+ * @param handler The handler to which the expiration message will be delivered.
+ * @param what The "what" parameter for the expiration message.
+ * @param label A name for this instance.
+ */
+ public AnrTimer(@NonNull Handler handler, int what, @NonNull String label) {
+ this(handler, what, label, false);
+ }
+
+ /**
+ * Return true if the service is enabled on this instance. Clients should use this method to
+ * decide if the feature is enabled, and not read the flags directly. This method should be
+ * deleted if and when the feature is enabled permanently.
+ *
+ * @return true if the service is flag-enabled.
+ */
+ public boolean serviceEnabled() {
+ return mFeature.enabled();
+ }
+
+ /**
+ * Start a trace on the timer. The trace is laid down in the AnrTimerTrack.
+ */
+ private void traceBegin(Timer t, String what) {
+ if (ENABLE_TRACING) {
+ final String label = formatSimple("%s(%d,%d,%s)", what, t.pid, t.uid, mLabel);
+ final int cookie = t.hashCode();
+ Trace.asyncTraceForTrackBegin(TRACE_TAG, TRACK, label, cookie);
+ }
+ }
+
+ /**
+ * End a trace on the timer.
+ */
+ private void traceEnd(Timer t) {
+ if (ENABLE_TRACING) {
+ final int cookie = t.hashCode();
+ Trace.asyncTraceForTrackEnd(TRACE_TAG, TRACK, cookie);
+ }
+ }
+
+ /**
+ * Return the string representation for a timer status.
+ */
+ private static String statusString(int s) {
+ switch (s) {
+ case TIMER_INVALID: return "invalid";
+ case TIMER_RUNNING: return "running";
+ case TIMER_EXPIRED: return "expired";
+ }
+ return formatSimple("unknown: %d", s);
+ }
+
+ /**
+ * Delete the timer associated with arg from the maps and return it. Return null if the timer
+ * was not found.
+ */
+ @GuardedBy("mLock")
+ private Timer removeLocked(V arg) {
+ Timer timer = mTimerMap.remove(arg);
+ return timer;
+ }
+
+ /**
+ * Return the number of timers currently running.
+ */
+ @VisibleForTesting
+ static int sizeOfTimerList() {
+ synchronized (sAnrTimerList) {
+ int totalTimers = 0;
+ for (int i = 0; i < sAnrTimerList.size(); i++) {
+ AnrTimer client = sAnrTimerList.get(i).get();
+ if (client != null) totalTimers += client.mTimerMap.size();
+ }
+ return totalTimers;
+ }
+ }
+
+ /**
+ * Clear out all existing timers. This will lead to unexpected behavior if used carelessly.
+ * It is available only for testing. It returns the number of times that were actually
+ * erased.
+ */
+ @VisibleForTesting
+ static int resetTimerListForHermeticTest() {
+ synchronized (sAnrTimerList) {
+ int mapLen = 0;
+ for (int i = 0; i < sAnrTimerList.size(); i++) {
+ AnrTimer client = sAnrTimerList.get(i).get();
+ if (client != null) {
+ mapLen += client.mTimerMap.size();
+ client.mTimerMap.clear();
+ }
+ }
+ if (mapLen > 0) {
+ Log.w(TAG, formatSimple("erasing timer list: clearing %d timers", mapLen));
+ }
+ return mapLen;
+ }
+ }
+
+ /**
+ * Generate a log message for a timer.
+ */
+ private void report(@NonNull Timer timer, @NonNull String msg) {
+ Log.i(TAG, msg + " " + timer + " " + Objects.toString(timer.arg));
+ }
+
+ /**
+ * The FeatureSwitch class provides a quick switch between feature-enabled behavior and
+ * feature-disabled behavior.
+ */
+ private abstract class FeatureSwitch {
+ abstract boolean start(@NonNull V arg, int pid, int uid, long timeoutMs);
+
+ abstract boolean cancel(@NonNull V arg);
+
+ abstract boolean accept(@NonNull V arg);
+
+ abstract boolean discard(@NonNull V arg);
+
+ abstract boolean enabled();
+ }
+
+ /**
+ * The FeatureDisabled class bypasses almost all AnrTimer logic. It is used when the AnrTimer
+ * service is disabled via Flags.anrTimerServiceEnabled.
+ */
+ private class FeatureDisabled extends FeatureSwitch {
+ /** Start a timer by sending a message to the client's handler. */
+ @Override
+ boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+ final Message msg = mHandler.obtainMessage(mWhat, arg);
+ mHandler.sendMessageDelayed(msg, timeoutMs);
+ return true;
+ }
+
+ /** Cancel a timer by removing the message from the client's handler. */
+ @Override
+ boolean cancel(@NonNull V arg) {
+ mHandler.removeMessages(mWhat, arg);
+ return true;
+ }
+
+ /** accept() is a no-op when the feature is disabled. */
+ @Override
+ boolean accept(@NonNull V arg) {
+ return true;
+ }
+
+ /** discard() is a no-op when the feature is disabled. */
+ @Override
+ boolean discard(@NonNull V arg) {
+ return true;
+ }
+
+ /** The feature is not enabled. */
+ @Override
+ boolean enabled() {
+ return false;
+ }
+ }
+
+ /**
+ * The FeatureEnabled class enables the AnrTimer logic. It is used when the AnrTimer service
+ * is enabled via Flags.anrTimerServiceEnabled.
+ */
+ private class FeatureEnabled extends FeatureSwitch {
+
+ /**
+ * Start a timer.
+ */
+ @Override
+ boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+ final Timer timer = Timer.obtain(pid, uid, arg, timeoutMs, AnrTimer.this);
+ synchronized (mLock) {
+ Timer old = mTimerMap.get(arg);
+ // There is an existing timer. If the timer was running, then cancel the running
+ // timer and restart it. If the timer was expired record a protocol error and
+ // discard the expired timer.
+ if (old != null) {
+ if (old.status == TIMER_EXPIRED) {
+ restartedLocked(old.status, arg);
+ discard(arg);
+ } else {
+ cancel(arg);
+ }
+ }
+ if (mTimerService.start(timer)) {
+ timer.status = TIMER_RUNNING;
+ mTimerMap.put(arg, timer);
+ mTotalStarted++;
+ mMaxStarted = Math.max(mMaxStarted, mTimerMap.size());
+ if (DEBUG) report(timer, "start");
+ return true;
+ } else {
+ Log.e(TAG, "AnrTimer.start failed");
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Cancel a timer. Return false if the timer was not found.
+ */
+ @Override
+ boolean cancel(@NonNull V arg) {
+ synchronized (mLock) {
+ Timer timer = removeLocked(arg);
+ if (timer == null) {
+ if (!mLenientCancel) notFoundLocked("cancel", arg);
+ return false;
+ }
+ mTimerService.cancel(timer);
+ // There may be an expiration message in flight. Cancel it.
+ mHandler.removeMessages(mWhat, arg);
+ if (DEBUG) report(timer, "cancel");
+ timer.release();
+ return true;
+ }
+ }
+
+ /**
+ * Accept a timer in the framework-level handler. The timeout has been accepted and the
+ * timeout handler is executing. Return false if the timer was not found.
+ */
+ @Override
+ boolean accept(@NonNull V arg) {
+ synchronized (mLock) {
+ Timer timer = removeLocked(arg);
+ if (timer == null) {
+ notFoundLocked("accept", arg);
+ return false;
+ }
+ mTimerService.accept(timer);
+ traceEnd(timer);
+ if (DEBUG) report(timer, "accept");
+ timer.release();
+ return true;
+ }
+ }
+
+ /**
+ * Discard a timer in the framework-level handler. For whatever reason, the timer is no
+ * longer interesting. No statistics are collected. Return false if the time was not
+ * found.
+ */
+ @Override
+ boolean discard(@NonNull V arg) {
+ synchronized (mLock) {
+ Timer timer = removeLocked(arg);
+ if (timer == null) {
+ notFoundLocked("discard", arg);
+ return false;
+ }
+ mTimerService.discard(timer);
+ traceEnd(timer);
+ if (DEBUG) report(timer, "discard");
+ timer.release();
+ return true;
+ }
+ }
+
+ /** The feature is enabled. */
+ @Override
+ boolean enabled() {
+ return true;
+ }
+ }
+
+ /**
+ * Start a timer associated with arg. The same object must be used to cancel, accept, or
+ * discard a timer later. If a timer already exists with the same arg, then the existing timer
+ * is canceled and a new timer is created.
+ *
+ * @param arg The key by which the timer is known. This is never examined or modified.
+ * @param pid The Linux process ID of the target being timed.
+ * @param uid The Linux user ID of the target being timed.
+ * @param timeoutMs The timer timeout, in milliseconds.
+ * @return true if the timer was successfully created.
+ */
+ public boolean start(@NonNull V arg, int pid, int uid, long timeoutMs) {
+ return mFeature.start(arg, pid, uid, timeoutMs);
+ }
+
+ /**
+ * Cancel the running timer associated with arg. The timer is forgotten. If the timer has
+ * expired, the call is treated as a discard. No errors are reported if the timer does not
+ * exist or if the timer has expired.
+ *
+ * @return true if the timer was found and was running.
+ */
+ public boolean cancel(@NonNull V arg) {
+ return mFeature.cancel(arg);
+ }
+
+ /**
+ * Accept the expired timer associated with arg. This indicates that the caller considers the
+ * timer expiration to be a true ANR. (See {@link #discard} for an alternate response.) It is
+ * an error to accept a running timer, however the running timer will be canceled.
+ *
+ * @return true if the timer was found and was expired.
+ */
+ public boolean accept(@NonNull V arg) {
+ return mFeature.accept(arg);
+ }
+
+ /**
+ * Discard the expired timer associated with arg. This indicates that the caller considers the
+ * timer expiration to be a false ANR. ((See {@link #accept} for an alternate response.) One
+ * reason to discard an expired timer is if the process being timed was also being debugged:
+ * such a process could be stopped at a breakpoint and its failure to respond would not be an
+ * error. It is an error to discard a running timer, however the running timer will be
+ * canceled.
+ *
+ * @return true if the timer was found and was expired.
+ */
+ public boolean discard(@NonNull V arg) {
+ return mFeature.discard(arg);
+ }
+
+ /**
+ * The notifier that a timer has fired. The timer is not modified.
+ */
+ @GuardedBy("mLock")
+ private void onExpiredLocked(@NonNull Timer timer) {
+ if (DEBUG) report(timer, "expire");
+ traceBegin(timer, "expired");
+ mHandler.sendMessage(Message.obtain(mHandler, mWhat, timer.arg));
+ synchronized (mLock) {
+ mTotalExpired++;
+ }
+ }
+
+ /**
+ * Dump a single AnrTimer.
+ */
+ private void dump(IndentingPrintWriter pw) {
+ synchronized (mLock) {
+ pw.format("timer: %s\n", mLabel);
+ pw.increaseIndent();
+ pw.format("started=%d maxStarted=%d running=%d expired=%d error=%d\n",
+ mTotalStarted, mMaxStarted, mTimerMap.size(),
+ mTotalExpired, mTotalErrors);
+ pw.decreaseIndent();
+ }
+ }
+
+ /**
+ * Enable or disable debugging.
+ */
+ static void debug(boolean f) {
+ DEBUG = f;
+ }
+
+ /**
+ * The current time in milliseconds.
+ */
+ private static long now() {
+ return SystemClock.uptimeMillis();
+ }
+
+ /**
+ * Log an error. A limited stack trace leading to the client call that triggered the error is
+ * recorded. The stack trace assumes that this method is not called directly.
+ *
+ * If DEBUG is true, a log message is generated as well.
+ */
+ @GuardedBy("mLock")
+ private void recordErrorLocked(String operation, String errorMsg, Object arg) {
+ StackTraceElement[] s = Thread.currentThread().getStackTrace();
+ final String what = Objects.toString(arg);
+ // The copy range starts at the caller of the timer operation, and includes three levels.
+ // This should be enough to isolate the location of the call.
+ StackTraceElement[] location = Arrays.copyOfRange(s, 6, 9);
+ synchronized (sErrors) {
+ sErrors.append(new Error(errorMsg, operation, mLabel, location, what));
+ }
+ if (DEBUG) Log.w(TAG, operation + " " + errorMsg + " " + mLabel + " timer " + what);
+ mTotalErrors++;
+ }
+
+ /**
+ * Log an error about a timer not found.
+ */
+ @GuardedBy("mLock")
+ private void notFoundLocked(String operation, Object arg) {
+ recordErrorLocked(operation, "notFound", arg);
+ }
+
+ /**
+ * Log an error about a timer that is started when there is an existing timer.
+ */
+ @GuardedBy("mLock")
+ private void restartedLocked(@TimerStatus int status, Object arg) {
+ recordErrorLocked("start", status == TIMER_EXPIRED ? "autoDiscard" : "autoCancel", arg);
+ }
+
+ /**
+ * Dump a single error to the output stream.
+ */
+ private static void dump(IndentingPrintWriter ipw, int seq, Error err) {
+ ipw.format("%2d: op:%s tag:%s issue:%s arg:%s\n", seq, err.operation, err.tag,
+ err.issue, err.arg);
+
+ final long offset = System.currentTimeMillis() - SystemClock.elapsedRealtime();
+ final long etime = offset + err.timestamp;
+ ipw.println(" date:" + TimeMigrationUtils.formatMillisWithFixedFormat(etime));
+ ipw.increaseIndent();
+ for (int i = 0; i < err.stack.length; i++) {
+ ipw.println(" " + err.stack[i].toString());
+ }
+ ipw.decreaseIndent();
+ }
+
+ /**
+ * Dump all errors to the output stream.
+ */
+ private static void dumpErrors(IndentingPrintWriter ipw) {
+ Error errors[];
+ synchronized (sErrors) {
+ if (sErrors.size() == 0) return;
+ errors = sErrors.toArray();
+ }
+ ipw.println("Errors");
+ ipw.increaseIndent();
+ for (int i = 0; i < errors.length; i++) {
+ if (errors[i] != null) dump(ipw, i, errors[i]);
+ }
+ ipw.decreaseIndent();
+ }
+
+ /**
+ * Dumpsys output.
+ */
+ public static void dump(@NonNull PrintWriter pw, boolean verbose) {
+ final IndentingPrintWriter ipw = new IndentingPrintWriter(pw);
+ ipw.println("AnrTimer statistics");
+ ipw.increaseIndent();
+ synchronized (sAnrTimerList) {
+ for (int i = 0; i < sAnrTimerList.size(); i++) {
+ AnrTimer client = sAnrTimerList.get(i).get();
+ if (client != null) client.dump(ipw);
+ }
+ }
+ if (verbose) dumpErrors(ipw);
+ ipw.format("AnrTimerEnd\n");
+ ipw.decreaseIndent();
+ }
+}
diff --git a/services/core/java/com/android/server/utils/flags.aconfig b/services/core/java/com/android/server/utils/flags.aconfig
new file mode 100644
index 000000000000..489e21ab06ca
--- /dev/null
+++ b/services/core/java/com/android/server/utils/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.utils"
+
+flag {
+ name: "anr_timer_service_enabled"
+ namespace: "system_performance"
+ is_fixed_read_only: true
+ description: "Feature flag for the ANR timer service"
+ bug: "282428924"
+}
diff --git a/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java
deleted file mode 100644
index 44d676052352..000000000000
--- a/services/tests/servicestests/src/com/android/server/am/AnrTimerTest.java
+++ /dev/null
@@ -1,389 +0,0 @@
-/*
- * Copyright (C) 2023 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.server.am;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import android.platform.test.annotations.Presubmit;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-
-import android.util.Log;
-
-import androidx.test.filters.SmallTest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Build/Install/Run:
- * atest FrameworksServicesTests:AnrTimerTest
- */
-@SmallTest
-@Presubmit
-public class AnrTimerTest {
-
- /**
- * A handler that allows control over when to dispatch messages and callbacks. Because most
- * Handler methods are final, the only thing this handler can intercept is sending messages.
- * This handler allows unit tests to be written without a need to sleep (which leads to flaky
- * tests).
- *
- * This code was cloned from {@link com.android.systemui.utils.os.FakeHandler}.
- */
- static class TestHandler extends Handler {
-
- private boolean mImmediate = true;
- private ArrayList mQueuedMessages = new ArrayList<>();
-
- ArrayList mDelays = new ArrayList<>();
-
- TestHandler(Looper looper, Callback callback, boolean immediate) {
- super(looper, callback);
- mImmediate = immediate;
- }
-
- TestHandler(Looper looper, Callback callback) {
- this(looper, callback, true);
- }
-
- /**
- * Override sendMessageAtTime. In immediate mode, the message is immediately dispatched.
- * In non-immediate mode, the message is enqueued to the real handler. In both cases, the
- * original delay is computed by comparing the target dispatch time with 'now'. This
- * computation is prone to errors if the code experiences delays. The computed time is
- * captured in the mDelays list.
- */
- @Override
- public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
- long delay = uptimeMillis - SystemClock.uptimeMillis();
- mDelays.add(delay);
- if (mImmediate) {
- mQueuedMessages.add(msg);
- dispatchQueuedMessages();
- } else {
- super.sendMessageAtTime(msg, uptimeMillis);
- }
- return true;
- }
-
- void setImmediate(boolean immediate) {
- mImmediate = immediate;
- }
-
- /** Dispatch any messages that have been queued on the calling thread. */
- void dispatchQueuedMessages() {
- ArrayList messages = new ArrayList<>(mQueuedMessages);
- mQueuedMessages.clear();
- for (Message msg : messages) {
- dispatchMessage(msg);
- }
- }
-
- /**
- * Compare the captured delays with the input array. The comparison is fuzzy because the
- * captured delay (see sendMessageAtTime) is affected by process delays.
- */
- void verifyDelays(long[] r) {
- final long FUZZ = 10;
- assertEquals(r.length, mDelays.size());
- for (int i = 0; i < mDelays.size(); i++) {
- long t = r[i];
- long v = mDelays.get(i);
- assertTrue(v >= t - FUZZ && v <= t + FUZZ);
- }
- }
- }
-
- private Handler mHandler;
- private CountDownLatch mLatch = null;
- private ArrayList mMessages;
-
- // The commonly used message timeout key.
- private static final int MSG_TIMEOUT = 1;
-
- @Before
- public void setUp() {
- mHandler = new Handler(Looper.getMainLooper(), this::expirationHandler);
- mMessages = new ArrayList<>();
- mLatch = new CountDownLatch(1);
- AnrTimer.resetTimerListForHermeticTest();
- }
-
- @After
- public void tearDown() {
- mHandler = null;
- mMessages = null;
- }
-
- // When a timer expires, set the expiration time in the message and add it to the queue.
- private boolean expirationHandler(Message msg) {
- mMessages.add(Message.obtain(msg));
- mLatch.countDown();
- return false;
- }
-
- // The test argument includes a pid and uid, and a tag. The tag is used to distinguish
- // different message instances.
- private static class TestArg {
- final int pid;
- final int uid;
- final int tag;
-
- TestArg(int pid, int uid, int tag) {
- this.pid = pid;
- this.uid = uid;
- this.tag = tag;
- }
- @Override
- public String toString() {
- return String.format("pid=%d uid=%d tag=%d", pid, uid, tag);
- }
- }
-
- /**
- * An instrumented AnrTimer.
- */
- private class TestAnrTimer extends AnrTimer {
- // A local copy of 'what'. The field in AnrTimer is private.
- final int mWhat;
-
- TestAnrTimer(Handler h, int key, String tag) {
- super(h, key, tag);
- mWhat = key;
- }
-
- TestAnrTimer() {
- this(mHandler, MSG_TIMEOUT, caller());
- }
-
- TestAnrTimer(Handler h, int key, String tag, boolean extend, TestInjector injector) {
- super(h, key, tag, extend, injector);
- mWhat = key;
- }
-
- TestAnrTimer(boolean extend, TestInjector injector) {
- this(mHandler, MSG_TIMEOUT, caller(), extend, injector);
- }
-
- // Return the name of method that called the constructor, assuming that this function is
- // called from inside the constructor. The calling method is used to name the AnrTimer
- // instance so that logs are easier to understand.
- private static String caller() {
- final int n = 4;
- StackTraceElement[] stack = Thread.currentThread().getStackTrace();
- if (stack.length < n+1) return "test";
- return stack[n].getMethodName();
- }
-
- boolean start(TestArg arg, long millis) {
- return start(arg, arg.pid, arg.uid, millis);
- }
-
- int what() {
- return mWhat;
- }
- }
-
- private static class TestTracker extends AnrTimer.CpuTracker {
- long index = 0;
- final int skip;
- TestTracker(int skip) {
- this.skip = skip;
- }
- long delay(int pid) {
- return index++ * skip;
- }
- }
-
- private class TestInjector extends AnrTimer.Injector {
- final boolean mImmediate;
- final AnrTimer.CpuTracker mTracker;
- TestHandler mTestHandler;
-
- TestInjector(int skip, boolean immediate) {
- super(mHandler);
- mTracker = new TestTracker(skip);
- mImmediate = immediate;
- }
-
- TestInjector(int skip) {
- this(skip, true);
- }
-
- @Override
- Handler newHandler(Handler.Callback callback) {
- if (mTestHandler == null) {
- mTestHandler = new TestHandler(mHandler.getLooper(), callback, mImmediate);
- }
- return mTestHandler;
- }
-
- /** Fetch the allocated handle. This does not check for nulls. */
- TestHandler getHandler() {
- return mTestHandler;
- }
-
- /**
- * This override returns the tracker supplied in the constructor. It does not create a
- * new one.
- */
- @Override
- AnrTimer.CpuTracker newTracker() {
- return mTracker;
- }
-
- /** For test purposes, always enable the feature. */
- @Override
- boolean isFeatureEnabled() {
- return true;
- }
- }
-
- // Tests
- // 1. Start a timer and wait for expiration.
- // 2. Start a timer and cancel it. Verify no expiration.
- // 3. Start a timer. Shortly thereafter, restart it. Verify only one expiration.
- // 4. Start a couple of timers. Verify max active timers. Discard one and verify the active
- // count drops by 1. Accept one and verify the active count drops by 1.
-
- @Test
- public void testSimpleTimeout() throws Exception {
- // Create an immediate TestHandler.
- TestInjector injector = new TestInjector(0);
- TestAnrTimer timer = new TestAnrTimer(false, injector);
- TestArg t = new TestArg(1, 1, 3);
- assertTrue(timer.start(t, 10));
- // Delivery is immediate but occurs on a different thread.
- assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS));
- assertEquals(1, mMessages.size());
- Message m = mMessages.get(0);
- assertEquals(timer.what(), m.what);
- assertEquals(t, m.obj);
-
- // Verify that the timer is still present.
- assertEquals(1, AnrTimer.sizeOfTimerList());
- assertTrue(timer.accept(t));
- assertEquals(0, AnrTimer.sizeOfTimerList());
-
- // Verify that the timer no longer exists.
- assertFalse(timer.accept(t));
- }
-
- @Test
- public void testCancel() throws Exception {
- // Create an non-immediate TestHandler.
- TestInjector injector = new TestInjector(0, false);
- TestAnrTimer timer = new TestAnrTimer(false, injector);
-
- Handler handler = injector.getHandler();
- assertNotNull(handler);
- assertTrue(handler instanceof TestHandler);
-
- // The tests that follow check for a 'what' of 0 (zero), which is the message key used
- // by AnrTimer internally.
- TestArg t = new TestArg(1, 1, 3);
- assertFalse(handler.hasMessages(0));
- assertTrue(timer.start(t, 100));
- assertTrue(handler.hasMessages(0));
- assertTrue(timer.cancel(t));
- assertFalse(handler.hasMessages(0));
-
- // Verify that no expiration messages were delivered.
- assertEquals(0, mMessages.size());
- assertEquals(0, AnrTimer.sizeOfTimerList());
- }
-
- @Test
- public void testRestart() throws Exception {
- // Create an non-immediate TestHandler.
- TestInjector injector = new TestInjector(0, false);
- TestAnrTimer timer = new TestAnrTimer(false, injector);
-
- TestArg t = new TestArg(1, 1, 3);
- assertTrue(timer.start(t, 2500));
- assertTrue(timer.start(t, 1000));
-
- // Verify that the test handler saw two timeouts.
- injector.getHandler().verifyDelays(new long[] { 2500, 1000 });
-
- // Verify that there is a single timer. Then cancel it.
- assertEquals(1, AnrTimer.sizeOfTimerList());
- assertTrue(timer.cancel(t));
- assertEquals(0, AnrTimer.sizeOfTimerList());
- }
-
- @Test
- public void testExtendNormal() throws Exception {
- // Create an immediate TestHandler.
- TestInjector injector = new TestInjector(5);
- TestAnrTimer timer = new TestAnrTimer(true, injector);
- TestArg t = new TestArg(1, 1, 3);
- assertTrue(timer.start(t, 10));
-
- assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS));
- assertEquals(1, mMessages.size());
- Message m = mMessages.get(0);
- assertEquals(timer.what(), m.what);
- assertEquals(t, m.obj);
-
- // Verify that the test handler saw two timeouts: one of 10ms and one of 5ms.
- injector.getHandler().verifyDelays(new long[] { 10, 5 });
-
- // Verify that the timer is still present. Then remove it and verify that the list is
- // empty.
- assertEquals(1, AnrTimer.sizeOfTimerList());
- assertTrue(timer.accept(t));
- assertEquals(0, AnrTimer.sizeOfTimerList());
- }
-
- @Test
- public void testExtendOversize() throws Exception {
- // Create an immediate TestHandler.
- TestInjector injector = new TestInjector(25);
- TestAnrTimer timer = new TestAnrTimer(true, injector);
- TestArg t = new TestArg(1, 1, 3);
- assertTrue(timer.start(t, 10));
-
- assertTrue(mLatch.await(100, TimeUnit.MILLISECONDS));
- assertEquals(1, mMessages.size());
- Message m = mMessages.get(0);
- assertEquals(timer.what(), m.what);
- assertEquals(t, m.obj);
-
- // Verify that the test handler saw two timeouts: one of 10ms and one of 10ms.
- injector.getHandler().verifyDelays(new long[] { 10, 10 });
-
- // Verify that the timer is still present. Then remove it and verify that the list is
- // empty.
- assertEquals(1, AnrTimer.sizeOfTimerList());
- assertTrue(timer.accept(t));
- assertEquals(0, AnrTimer.sizeOfTimerList());
- }
-}
diff --git a/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
new file mode 100644
index 000000000000..330dbb83e949
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2023 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.server.utils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import android.platform.test.annotations.Presubmit;
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.annotations.GuardedBy;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+@Presubmit
+public class AnrTimerTest {
+
+ // The commonly used message timeout key.
+ private static final int MSG_TIMEOUT = 1;
+
+ // The test argument includes a pid and uid, and a tag. The tag is used to distinguish
+ // different message instances. Additional fields (like what) capture delivery information
+ // that is checked by the test.
+ private static class TestArg {
+ final int pid;
+ final int uid;
+ int what;
+
+ TestArg(int pid, int uid) {
+ this.pid = pid;
+ this.uid = uid;
+ this.what = 0;
+ }
+ }
+
+ /**
+ * The test handler is a self-contained object for a single test.
+ */
+ private static class Helper {
+ final Object mLock = new Object();
+
+ final Handler mHandler;
+ final CountDownLatch mLatch;
+ @GuardedBy("mLock")
+ final ArrayList mMessages;
+
+ Helper(int expect) {
+ mHandler = new Handler(Looper.getMainLooper(), this::expirationHandler);
+ mMessages = new ArrayList<>();
+ mLatch = new CountDownLatch(expect);
+ }
+
+ /**
+ * When a timer expires, the object must be a TestArg. Update the TestArg with
+ * expiration metadata and save it.
+ */
+ private boolean expirationHandler(Message msg) {
+ synchronized (mLock) {
+ TestArg arg = (TestArg) msg.obj;
+ arg.what = msg.what;
+ mMessages.add(arg);
+ mLatch.countDown();
+ return false;
+ }
+ }
+
+ boolean await(long timeout) throws InterruptedException {
+ // No need to synchronize, as the CountDownLatch is already thread-safe.
+ return mLatch.await(timeout, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * Fetch the received messages. Fail if the count of received messages is other than the
+ * expected count.
+ */
+ TestArg[] messages(int expected) {
+ synchronized (mLock) {
+ assertEquals(expected, mMessages.size());
+ return mMessages.toArray(new TestArg[expected]);
+ }
+ }
+ }
+
+ /**
+ * An instrumented AnrTimer.
+ */
+ private static class TestAnrTimer extends AnrTimer {
+ private TestAnrTimer(Handler h, int key, String tag) {
+ super(h, key, tag);
+ }
+
+ TestAnrTimer(Helper helper) {
+ this(helper.mHandler, MSG_TIMEOUT, caller());
+ }
+
+ void start(TestArg arg, long millis) {
+ start(arg, arg.pid, arg.uid, millis);
+ }
+
+ // Return the name of method that called the constructor, assuming that this function is
+ // called from inside the constructor. The calling method is used to name the AnrTimer
+ // instance so that logs are easier to understand.
+ private static String caller() {
+ final int n = 4;
+ StackTraceElement[] stack = Thread.currentThread().getStackTrace();
+ if (stack.length < n+1) return "test";
+ return stack[n].getMethodName();
+ }
+ }
+
+ void validate(TestArg expected, TestArg actual) {
+ assertEquals(expected, actual);
+ assertEquals(actual.what, MSG_TIMEOUT);
+ }
+
+
+ /**
+ * Verify that a simple expiration succeeds. The timer is started for 10ms. The test
+ * procedure waits 5s for the expiration message, but under correct operation, the test will
+ * only take 10ms
+ */
+ @Test
+ public void testSimpleTimeout() throws Exception {
+ Helper helper = new Helper(1);
+ TestAnrTimer timer = new TestAnrTimer(helper);
+ TestArg t = new TestArg(1, 1);
+ timer.start(t, 10);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(1);
+ validate(t, result[0]);
+ }
+
+ /**
+ * Verify that if three timers are scheduled, they are delivered in time order.
+ */
+ @Test
+ public void testMultipleTimers() throws Exception {
+ // Expect three messages.
+ Helper helper = new Helper(3);
+ TestAnrTimer timer = new TestAnrTimer(helper);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ timer.start(t1, 50);
+ timer.start(t2, 60);
+ timer.start(t3, 40);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(3);
+ validate(t3, result[0]);
+ validate(t1, result[1]);
+ validate(t2, result[2]);
+ }
+
+ /**
+ * Verify that a canceled timer is not delivered.
+ */
+ @Test
+ public void testCancelTimer() throws Exception {
+ // Expect two messages.
+ Helper helper = new Helper(2);
+ TestAnrTimer timer = new TestAnrTimer(helper);
+ TestArg t1 = new TestArg(1, 1);
+ TestArg t2 = new TestArg(1, 2);
+ TestArg t3 = new TestArg(1, 3);
+ timer.start(t1, 50);
+ timer.start(t2, 60);
+ timer.start(t3, 40);
+ // Briefly pause.
+ assertFalse(helper.await(10));
+ timer.cancel(t1);
+ // Delivery is immediate but occurs on a different thread.
+ assertTrue(helper.await(5000));
+ TestArg[] result = helper.messages(2);
+ validate(t3, result[0]);
+ validate(t2, result[1]);
+ }
+}
--
cgit v1.2.3-59-g8ed1b