Loading core/java/com/android/internal/os/TimeoutRecord.java +8 −0 Original line number Diff line number Diff line Loading @@ -220,6 +220,14 @@ public class TimeoutRecord { return this; } /** * Return the expired timer. */ @Nullable public AutoCloseable getExpiredTimer() { return mExpiredTimer; } /** Close the ExpiredTimer, if one is present. getExpiredTimer will return null after this. */ public void closeExpiredTimer() { try { Loading services/core/java/com/android/server/utils/AnrTimer.java +64 −22 Original line number Diff line number Diff line Loading @@ -301,10 +301,10 @@ public class AnrTimer<V> implements AutoCloseable { /** * A target process may be modified when its timer expires. The modification (if any) will be * undone if the expiration is discarded, but is persisted if the expiration is accepted. If * the expiration is accepted, then a TimerLock is returned to the client. The client must * close the TimerLock to complete the state machine. * the expiration is accepted, then a ExpiredTimer is returned to the client. The client must * close the ExpiredTimer to complete the state machine. */ private class TimerLock implements AutoCloseable { public static class ExpiredTimer implements AutoCloseable { // Detect failures to close. private final CloseGuard mGuard = new CloseGuard(); Loading @@ -314,7 +314,11 @@ public class AnrTimer<V> implements AutoCloseable { // Allow multiple calls to close(). private boolean mClosed = false; TimerLock() { // The timer ID. final int mTimerId; ExpiredTimer(int id) { mTimerId = id; mGuard.open("AnrTimer.release"); } Loading Loading @@ -401,10 +405,14 @@ public class AnrTimer<V> implements AutoCloseable { @GuardedBy("mLock") private final ArrayMap<V, Integer> mTimerIdMap = new ArrayMap<>(); /** Reverse map from timer ID to client argument. */ /** Reverse map from timer ID to client argument, needed by the expire() callback. */ @GuardedBy("mLock") private final SparseArray<V> mTimerArgMap = new SparseArray<>(); /** Map from timer ID to ExpiredTimer. */ @GuardedBy("mLock") private final SparseArray<ExpiredTimer> mExpiredTimers = new SparseArray<>(); /** The highwater mark of started, but not closed, timers. */ @GuardedBy("mLock") private int mMaxStarted = 0; Loading Loading @@ -528,7 +536,7 @@ public class AnrTimer<V> implements AutoCloseable { abstract boolean cancel(@NonNull V arg); @Nullable abstract TimerLock accept(@NonNull V arg); abstract ExpiredTimer accept(@NonNull V arg); abstract boolean discard(@NonNull V arg); Loading Loading @@ -564,7 +572,7 @@ public class AnrTimer<V> implements AutoCloseable { /** accept() is a no-op when the feature is disabled. */ @Override @Nullable TimerLock accept(@NonNull V arg) { ExpiredTimer accept(@NonNull V arg) { return null; } Loading Loading @@ -684,28 +692,31 @@ public class AnrTimer<V> implements AutoCloseable { /** * Accept a timer in the framework-level handler. The timeout has been accepted and the * client's timeout handler is executing. If the function returns a non-null TimerLock then * the associated process may have been paused (or otherwise modified in preparation for * debugging). The TimerLock must be closed to allow the process to continue, or to be * dumped in an AnrReport. * client's timeout handler is executing. If the function returns a non-null ExpiredTimer * then the associated process may have been paused (or otherwise modified in preparation * for debugging). The ExpiredTimer must be closed. */ @Override @Nullable TimerLock accept(@NonNull V arg) { ExpiredTimer accept(@NonNull V arg) { synchronized (mLock) { Integer timer = removeLocked(arg); ExpiredTimer timer = removeLockedTimer(arg); if (timer == null) { notFoundLocked("accept", arg); return null; } // Race conditions may lead to timer acceptance after the service was closed. if (mNative == 0) return null; boolean accepted = nativeAnrTimerAccept(mNative, timer); boolean accepted = nativeAnrTimerAccept(mNative, timer.mTimerId); trace("accept", timer); // If "accepted" is true then the native layer has pending operations against this // timer. Wrap the timer ID in a TimerLock and return it to the caller. If // timer. Wrap the timer ID in a ExpiredTimer and return it to the caller. If // "accepted" is false then the native later does not have any pending operations. return accepted ? new TimerLock() : null; if (!accepted) { timer.close(); timer = null; } return timer; } } Loading Loading @@ -770,15 +781,32 @@ public class AnrTimer<V> implements AutoCloseable { } } /** * Delete the entries associated with arg from the maps and return the ExpiredTimer of the * timer, if any. */ @GuardedBy("mLock") private ExpiredTimer removeLockedTimer(V arg) { final Integer r = mTimerIdMap.remove(arg); ExpiredTimer l = null; if (r != null) { mTimerArgMap.remove(r); l = mExpiredTimers.removeReturnOld(r); } return l; } /** * Delete the entries associated with arg from the maps and return the ID of the timer, if * any. * any. If there is a ExpiredTimer present, it is closed. */ @GuardedBy("mLock") private Integer removeLocked(V arg) { Integer r = mTimerIdMap.remove(arg); final Integer r = mTimerIdMap.remove(arg); if (r != null) { mTimerArgMap.remove(r); ExpiredTimer l = mExpiredTimers.removeReturnOld(r); if (l != null) l.close(); } return r; } Loading Loading @@ -843,9 +871,9 @@ public class AnrTimer<V> implements AutoCloseable { /** * 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.) The * function stores a {@link TimerLock} in the {@link TimeoutRecord} argument. The TimerLock * records information about the expired timer for retrieval during ANR report generation. * After this call, the timer does not exist. * function stores a {@link ExpiredTimer} in the {@link TimeoutRecord} argument. The * ExpiredTimer records information about the expired timer for retrieval during ANR report * generation. After this call, the timer does not exist. * * It is a protocol error to accept a running timer, however, the running timer will be * canceled. Loading Loading @@ -887,7 +915,7 @@ public class AnrTimer<V> implements AutoCloseable { @Keep private boolean expire(int timerId, int pid, int uid, long elapsedMs) { trace("expired", timerId, pid, uid, mLabel, elapsedMs); V arg = null; final V arg; synchronized (mLock) { arg = mTimerArgMap.get(timerId); if (arg == null) { Loading @@ -896,6 +924,7 @@ public class AnrTimer<V> implements AutoCloseable { mTotalErrors++; return false; } mExpiredTimers.put(timerId, new ExpiredTimer(timerId)); mTotalExpired++; } final Message msg = Message.obtain(mHandler, mWhat, arg); Loading Loading @@ -965,6 +994,19 @@ public class AnrTimer<V> implements AutoCloseable { mFeature.setTime(now); } /** * Return the ExpiredTimer associated with a TimeoutRecord. The TimeoutRecord is not modified. */ @Nullable public static ExpiredTimer expiredTimer(TimeoutRecord tr) { AutoCloseable expiredTimer = tr.getExpiredTimer(); if (expiredTimer instanceof ExpiredTimer lock) { return lock; } else { return null; } } /** * Ensure any native resources are freed when the object is GC'ed. Best practice is to close * the object explicitly, but overriding finalize() avoids accidental leaks. Loading services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java +44 −1 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import android.util.Log; import androidx.test.filters.SmallTest; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.TimeoutRecord; import org.junit.Ignore; import org.junit.Test; Loading @@ -52,7 +53,7 @@ public class AnrTimerTest { 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 // different message instances. Additional fields (like "what") capture delivery information // that is checked by the test. private static class TestArg { final int pid; Loading Loading @@ -531,6 +532,48 @@ public class AnrTimerTest { } } /** * Test the ExpiredTimer feature. */ @Test public void testExpiredTimer() throws Exception { assumeTrue(AnrTimer.nativeTimersSupported()); AnrTimer.Args args = new AnrTimer.Args() .enable(true) .testMode(true); Helper helper = new Helper(1); TestArg t1 = new TestArg(1, 1); TestArg t2 = new TestArg(2, 2); try (TestAnrTimer timer = new TestAnrTimer(helper, args)) { Stepper stepper = new Stepper(timer, helper); timer.start(t1, 100); stepper.stepAndWait(100); timer.start(t2, 100); stepper.stepAndWait(200); TestResult[] result = helper.results(2); assertThat(timer.discard(t1)).isTrue(); assertThat(timer.discard(t1)).isFalse(); TimeoutRecord tr1a = TimeoutRecord.forApp("testing purposes"); timer.accept(t1, tr1a); assertThat(tr1a.getExpiredTimer()).isNull(); TimeoutRecord tr2a = TimeoutRecord.forApp("testing purposes"); timer.accept(t2, tr2a); assertThat(tr2a.getExpiredTimer()).isNotNull(); assertThat(tr2a.getExpiredTimer()).isNotNull(); tr2a.closeExpiredTimer(); assertThat(tr2a.getExpiredTimer()).isNull(); assertThat(timer.discard(t2)).isFalse(); TimeoutRecord tr2b = TimeoutRecord.forApp("testing purposes"); timer.accept(t2, tr2b); assertThat(tr2b.getExpiredTimer()).isNull(); } } /** * Return the dump string. */ Loading Loading
core/java/com/android/internal/os/TimeoutRecord.java +8 −0 Original line number Diff line number Diff line Loading @@ -220,6 +220,14 @@ public class TimeoutRecord { return this; } /** * Return the expired timer. */ @Nullable public AutoCloseable getExpiredTimer() { return mExpiredTimer; } /** Close the ExpiredTimer, if one is present. getExpiredTimer will return null after this. */ public void closeExpiredTimer() { try { Loading
services/core/java/com/android/server/utils/AnrTimer.java +64 −22 Original line number Diff line number Diff line Loading @@ -301,10 +301,10 @@ public class AnrTimer<V> implements AutoCloseable { /** * A target process may be modified when its timer expires. The modification (if any) will be * undone if the expiration is discarded, but is persisted if the expiration is accepted. If * the expiration is accepted, then a TimerLock is returned to the client. The client must * close the TimerLock to complete the state machine. * the expiration is accepted, then a ExpiredTimer is returned to the client. The client must * close the ExpiredTimer to complete the state machine. */ private class TimerLock implements AutoCloseable { public static class ExpiredTimer implements AutoCloseable { // Detect failures to close. private final CloseGuard mGuard = new CloseGuard(); Loading @@ -314,7 +314,11 @@ public class AnrTimer<V> implements AutoCloseable { // Allow multiple calls to close(). private boolean mClosed = false; TimerLock() { // The timer ID. final int mTimerId; ExpiredTimer(int id) { mTimerId = id; mGuard.open("AnrTimer.release"); } Loading Loading @@ -401,10 +405,14 @@ public class AnrTimer<V> implements AutoCloseable { @GuardedBy("mLock") private final ArrayMap<V, Integer> mTimerIdMap = new ArrayMap<>(); /** Reverse map from timer ID to client argument. */ /** Reverse map from timer ID to client argument, needed by the expire() callback. */ @GuardedBy("mLock") private final SparseArray<V> mTimerArgMap = new SparseArray<>(); /** Map from timer ID to ExpiredTimer. */ @GuardedBy("mLock") private final SparseArray<ExpiredTimer> mExpiredTimers = new SparseArray<>(); /** The highwater mark of started, but not closed, timers. */ @GuardedBy("mLock") private int mMaxStarted = 0; Loading Loading @@ -528,7 +536,7 @@ public class AnrTimer<V> implements AutoCloseable { abstract boolean cancel(@NonNull V arg); @Nullable abstract TimerLock accept(@NonNull V arg); abstract ExpiredTimer accept(@NonNull V arg); abstract boolean discard(@NonNull V arg); Loading Loading @@ -564,7 +572,7 @@ public class AnrTimer<V> implements AutoCloseable { /** accept() is a no-op when the feature is disabled. */ @Override @Nullable TimerLock accept(@NonNull V arg) { ExpiredTimer accept(@NonNull V arg) { return null; } Loading Loading @@ -684,28 +692,31 @@ public class AnrTimer<V> implements AutoCloseable { /** * Accept a timer in the framework-level handler. The timeout has been accepted and the * client's timeout handler is executing. If the function returns a non-null TimerLock then * the associated process may have been paused (or otherwise modified in preparation for * debugging). The TimerLock must be closed to allow the process to continue, or to be * dumped in an AnrReport. * client's timeout handler is executing. If the function returns a non-null ExpiredTimer * then the associated process may have been paused (or otherwise modified in preparation * for debugging). The ExpiredTimer must be closed. */ @Override @Nullable TimerLock accept(@NonNull V arg) { ExpiredTimer accept(@NonNull V arg) { synchronized (mLock) { Integer timer = removeLocked(arg); ExpiredTimer timer = removeLockedTimer(arg); if (timer == null) { notFoundLocked("accept", arg); return null; } // Race conditions may lead to timer acceptance after the service was closed. if (mNative == 0) return null; boolean accepted = nativeAnrTimerAccept(mNative, timer); boolean accepted = nativeAnrTimerAccept(mNative, timer.mTimerId); trace("accept", timer); // If "accepted" is true then the native layer has pending operations against this // timer. Wrap the timer ID in a TimerLock and return it to the caller. If // timer. Wrap the timer ID in a ExpiredTimer and return it to the caller. If // "accepted" is false then the native later does not have any pending operations. return accepted ? new TimerLock() : null; if (!accepted) { timer.close(); timer = null; } return timer; } } Loading Loading @@ -770,15 +781,32 @@ public class AnrTimer<V> implements AutoCloseable { } } /** * Delete the entries associated with arg from the maps and return the ExpiredTimer of the * timer, if any. */ @GuardedBy("mLock") private ExpiredTimer removeLockedTimer(V arg) { final Integer r = mTimerIdMap.remove(arg); ExpiredTimer l = null; if (r != null) { mTimerArgMap.remove(r); l = mExpiredTimers.removeReturnOld(r); } return l; } /** * Delete the entries associated with arg from the maps and return the ID of the timer, if * any. * any. If there is a ExpiredTimer present, it is closed. */ @GuardedBy("mLock") private Integer removeLocked(V arg) { Integer r = mTimerIdMap.remove(arg); final Integer r = mTimerIdMap.remove(arg); if (r != null) { mTimerArgMap.remove(r); ExpiredTimer l = mExpiredTimers.removeReturnOld(r); if (l != null) l.close(); } return r; } Loading Loading @@ -843,9 +871,9 @@ public class AnrTimer<V> implements AutoCloseable { /** * 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.) The * function stores a {@link TimerLock} in the {@link TimeoutRecord} argument. The TimerLock * records information about the expired timer for retrieval during ANR report generation. * After this call, the timer does not exist. * function stores a {@link ExpiredTimer} in the {@link TimeoutRecord} argument. The * ExpiredTimer records information about the expired timer for retrieval during ANR report * generation. After this call, the timer does not exist. * * It is a protocol error to accept a running timer, however, the running timer will be * canceled. Loading Loading @@ -887,7 +915,7 @@ public class AnrTimer<V> implements AutoCloseable { @Keep private boolean expire(int timerId, int pid, int uid, long elapsedMs) { trace("expired", timerId, pid, uid, mLabel, elapsedMs); V arg = null; final V arg; synchronized (mLock) { arg = mTimerArgMap.get(timerId); if (arg == null) { Loading @@ -896,6 +924,7 @@ public class AnrTimer<V> implements AutoCloseable { mTotalErrors++; return false; } mExpiredTimers.put(timerId, new ExpiredTimer(timerId)); mTotalExpired++; } final Message msg = Message.obtain(mHandler, mWhat, arg); Loading Loading @@ -965,6 +994,19 @@ public class AnrTimer<V> implements AutoCloseable { mFeature.setTime(now); } /** * Return the ExpiredTimer associated with a TimeoutRecord. The TimeoutRecord is not modified. */ @Nullable public static ExpiredTimer expiredTimer(TimeoutRecord tr) { AutoCloseable expiredTimer = tr.getExpiredTimer(); if (expiredTimer instanceof ExpiredTimer lock) { return lock; } else { return null; } } /** * Ensure any native resources are freed when the object is GC'ed. Best practice is to close * the object explicitly, but overriding finalize() avoids accidental leaks. Loading
services/tests/servicestests/src/com/android/server/utils/AnrTimerTest.java +44 −1 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import android.util.Log; import androidx.test.filters.SmallTest; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.TimeoutRecord; import org.junit.Ignore; import org.junit.Test; Loading @@ -52,7 +53,7 @@ public class AnrTimerTest { 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 // different message instances. Additional fields (like "what") capture delivery information // that is checked by the test. private static class TestArg { final int pid; Loading Loading @@ -531,6 +532,48 @@ public class AnrTimerTest { } } /** * Test the ExpiredTimer feature. */ @Test public void testExpiredTimer() throws Exception { assumeTrue(AnrTimer.nativeTimersSupported()); AnrTimer.Args args = new AnrTimer.Args() .enable(true) .testMode(true); Helper helper = new Helper(1); TestArg t1 = new TestArg(1, 1); TestArg t2 = new TestArg(2, 2); try (TestAnrTimer timer = new TestAnrTimer(helper, args)) { Stepper stepper = new Stepper(timer, helper); timer.start(t1, 100); stepper.stepAndWait(100); timer.start(t2, 100); stepper.stepAndWait(200); TestResult[] result = helper.results(2); assertThat(timer.discard(t1)).isTrue(); assertThat(timer.discard(t1)).isFalse(); TimeoutRecord tr1a = TimeoutRecord.forApp("testing purposes"); timer.accept(t1, tr1a); assertThat(tr1a.getExpiredTimer()).isNull(); TimeoutRecord tr2a = TimeoutRecord.forApp("testing purposes"); timer.accept(t2, tr2a); assertThat(tr2a.getExpiredTimer()).isNotNull(); assertThat(tr2a.getExpiredTimer()).isNotNull(); tr2a.closeExpiredTimer(); assertThat(tr2a.getExpiredTimer()).isNull(); assertThat(timer.discard(t2)).isFalse(); TimeoutRecord tr2b = TimeoutRecord.forApp("testing purposes"); timer.accept(t2, tr2b); assertThat(tr2b.getExpiredTimer()).isNull(); } } /** * Return the dump string. */ Loading