Merge changes I756cb844,I79b63c68 into main

* changes:
  Rename TimerFileDescriptor to RealtimeScheduler
  Enhance TimerFileDescriptor to schedule multiple tasks
This commit is contained in:
Treehugger Robot 2025-01-09 18:29:29 -08:00 • committed by Gerrit Code Review
commit 12d6da67a0
4 changed files with 265 additions and 168 deletions

View file

@ -0,0 +1,324 @@
/*
* Copyright (C) 2024 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.net.module.util;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.ParcelFileDescriptor;
import android.os.SystemClock;
import android.util.CloseGuard;
import android.util.Log;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.PriorityQueue;
/**
* Represents a realtime scheduler object used for scheduling tasks with precise delays.
* Compared to {@link Handler#postDelayed}, this class offers enhanced accuracy for delayed
* callbacks by accounting for periods when the device is in deep sleep.
*
* <p> This class is designed for use exclusively from the handler thread.
*
* **Usage Examples:**
*
* ** Scheduling recurring tasks with the same RealtimeScheduler **
*
* ```java
* // Create a RealtimeScheduler
* final RealtimeScheduler scheduler = new RealtimeScheduler(handler);
*
* // Schedule a new task with a delay.
* scheduler.postDelayed(() -> taskToExecute(), delayTime);
*
* // Once the delay has elapsed, and the task is running, schedule another task.
* scheduler.postDelayed(() -> anotherTaskToExecute(), anotherDelayTime);
*
* // Remember to close the RealtimeScheduler after all tasks have finished running.
* scheduler.close();
* ```
*/
public class RealtimeScheduler {
private static final String TAG = RealtimeScheduler.class.getSimpleName();
// EVENT_ERROR may be generated even if not specified, as per its javadoc.
private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
private final CloseGuard mGuard = new CloseGuard();
@NonNull
private final Handler mHandler;
@NonNull
private final MessageQueue mQueue;
@NonNull
private final ParcelFileDescriptor mParcelFileDescriptor;
private final int mFdInt;
private final PriorityQueue<Task> mTaskQueue;
/**
* An abstract class for defining tasks that can be executed using a {@link Handler}.
*/
private abstract static class Task implements Comparable<Task> {
private final long mRunTimeMs;
private final long mCreatedTimeNs = SystemClock.elapsedRealtimeNanos();
/**
* create a task with a run time
*/
Task(long runTimeMs) {
mRunTimeMs = runTimeMs;
}
/**
* Executes the task using the provided {@link Handler}.
*
* @param handler The {@link Handler} to use for executing the task.
*/
abstract void post(Handler handler);
@Override
public int compareTo(@NonNull Task o) {
if (mRunTimeMs != o.mRunTimeMs) {
return Long.compare(mRunTimeMs, o.mRunTimeMs);
}
return Long.compare(mCreatedTimeNs, o.mCreatedTimeNs);
}
/**
* Returns the run time of the task.
*/
public long getRunTimeMs() {
return mRunTimeMs;
}
}
/**
* A task that sends a {@link Message} using a {@link Handler}.
*/
private static class MessageTask extends Task {
private final Message mMessage;
MessageTask(Message message, long runTimeMs) {
super(runTimeMs);
mMessage = message;
}
/**
* Sends the {@link Message} using the provided {@link Handler}.
*
* @param handler The {@link Handler} to use for sending the message.
*/
@Override
public void post(Handler handler) {
handler.sendMessage(mMessage);
}
}
/**
* A task that posts a {@link Runnable} to a {@link Handler}.
*/
private static class RunnableTask extends Task {
private final Runnable mRunnable;
RunnableTask(Runnable runnable, long runTimeMs) {
super(runTimeMs);
mRunnable = runnable;
}
/**
* Posts the {@link Runnable} to the provided {@link Handler}.
*
* @param handler The {@link Handler} to use for posting the runnable.
*/
@Override
public void post(Handler handler) {
handler.post(mRunnable);
}
}
/**
* The RealtimeScheduler constructor
*
* Note: The constructor is currently safe to call on another thread because it only sets final
* members and registers the event to be called on the handler.
*/
public RealtimeScheduler(@NonNull Handler handler) {
mFdInt = TimerFdUtils.createTimerFileDescriptor();
mParcelFileDescriptor = ParcelFileDescriptor.adoptFd(mFdInt);
mHandler = handler;
mQueue = handler.getLooper().getQueue();
mTaskQueue = new PriorityQueue<>();
registerFdEventListener();
mGuard.open("close");
}
private boolean enqueueTask(@NonNull Task task, long delayMs) {
ensureRunningOnCorrectThread();
if (delayMs <= 0L) {
task.post(mHandler);
return true;
}
if (mTaskQueue.isEmpty() || task.compareTo(mTaskQueue.peek()) < 0) {
if (!TimerFdUtils.setExpirationTime(mFdInt, delayMs)) {
return false;
}
}
mTaskQueue.add(task);
return true;
}
/**
* Set a runnable to be executed after a specified delay.
*
* If delayMs is less than or equal to 0, the runnable will be executed immediately.
*
* @param runnable the runnable to be executed
* @param delayMs the delay time in milliseconds
* @return true if the task is scheduled successfully, false otherwise.
*/
public boolean postDelayed(@NonNull Runnable runnable, long delayMs) {
return enqueueTask(new RunnableTask(runnable, SystemClock.elapsedRealtime() + delayMs),
delayMs);
}
/**
* Remove a scheduled runnable.
*
* @param runnable the runnable to be removed
*/
public void removeDelayedRunnable(@NonNull Runnable runnable) {
ensureRunningOnCorrectThread();
mTaskQueue.removeIf(task -> task instanceof RunnableTask
&& ((RunnableTask) task).mRunnable == runnable);
}
/**
* Set a message to be sent after a specified delay.
*
* If delayMs is less than or equal to 0, the message will be sent immediately.
*
* @param msg the message to be sent
* @param delayMs the delay time in milliseconds
* @return true if the message is scheduled successfully, false otherwise.
*/
public boolean sendDelayedMessage(Message msg, long delayMs) {
return enqueueTask(new MessageTask(msg, SystemClock.elapsedRealtime() + delayMs), delayMs);
}
/**
* Remove a scheduled message.
*
* @param what the message to be removed
*/
public void removeDelayedMessage(int what) {
ensureRunningOnCorrectThread();
mTaskQueue.removeIf(task -> task instanceof MessageTask
&& ((MessageTask) task).mMessage.what == what);
}
/**
* Close the RealtimeScheduler. This implementation closes the underlying
* OS resources allocated to represent this stream.
*/
public void close() {
ensureRunningOnCorrectThread();
unregisterAndDestroyFd();
}
private void registerFdEventListener() {
mQueue.addOnFileDescriptorEventListener(
mParcelFileDescriptor.getFileDescriptor(),
FD_EVENTS,
(fd, events) -> {
if (!isRunning()) {
return 0;
}
if ((events & EVENT_INPUT) != 0) {
handleExpiration();
}
return FD_EVENTS;
});
}
private boolean isRunning() {
return mParcelFileDescriptor.getFileDescriptor().valid();
}
private void handleExpiration() {
long currentTimeMs = SystemClock.elapsedRealtime();
while (!mTaskQueue.isEmpty()) {
final Task task = mTaskQueue.peek();
currentTimeMs = SystemClock.elapsedRealtime();
if (currentTimeMs < task.getRunTimeMs()) {
break;
}
task.post(mHandler);
mTaskQueue.poll();
}
if (!mTaskQueue.isEmpty()) {
// Using currentTimeMs ensures that the calculated expiration time
// is always positive.
if (!TimerFdUtils.setExpirationTime(mFdInt,
mTaskQueue.peek().getRunTimeMs() - currentTimeMs)) {
// If setting the expiration time fails, clear the task queue.
Log.wtf(TAG, "Failed to set expiration time");
mTaskQueue.clear();
}
} else {
// We have to clean up the timer if no tasks are left. Otherwise, the timer will keep
// being triggered.
TimerFdUtils.setExpirationTime(mFdInt, 0);
}
}
private void unregisterAndDestroyFd() {
if (mGuard != null) {
mGuard.close();
}
mQueue.removeOnFileDescriptorEventListener(mParcelFileDescriptor.getFileDescriptor());
try {
mParcelFileDescriptor.close();
} catch (IOException exception) {
Log.e(TAG, "close ParcelFileDescriptor failed. ", exception);
}
}
private void ensureRunningOnCorrectThread() {
if (mHandler.getLooper() != Looper.myLooper()) {
throw new IllegalStateException(
"Not running on Handler thread: " + Thread.currentThread().getName());
}
}
@SuppressWarnings("Finalize")
@Override
protected void finalize() throws Throwable {
if (mGuard != null) {
mGuard.warnIfOpen();
}
super.finalize();
}
}

View file

@ -28,6 +28,7 @@ android_library {
"net-utils-device-common-struct-base",
"net-utils-device-common-wear",
"net-utils-service-connectivity",
"truth",
],
libs: [
"android.test.runner.stubs",

View file

@ -0,0 +1,140 @@
/*
* Copyright (C) 2024 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.net.module.util
import android.os.Build
import android.os.ConditionVariable
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Message
import android.os.SystemClock
import androidx.test.filters.SmallTest
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.tryTest
import com.android.testutils.visibleOnHandlerThread
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertEquals
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class RealtimeSchedulerTest {
private val TIMEOUT_MS = 1000L
private val TOLERANCE_MS = 50L
private class TestHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(msg: Message) {
val pair = msg.obj as Pair<ConditionVariable, MutableList<Long>>
val cv = pair.first
cv.open()
val executionTimes = pair.second
executionTimes.add(SystemClock.elapsedRealtime())
}
}
private val thread = HandlerThread(RealtimeSchedulerTest::class.simpleName).apply { start() }
private val handler by lazy { TestHandler(thread.looper) }
@After
fun tearDown() {
thread.quitSafely()
thread.join()
}
@Test
fun testMultiplePostDelayedTasks() {
val scheduler = RealtimeScheduler(handler)
tryTest {
val initialTimeMs = SystemClock.elapsedRealtime()
val executionTimes = mutableListOf<Long>()
val cv = ConditionVariable()
handler.post {
scheduler.postDelayed(
{ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 0)
scheduler.postDelayed(
{ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 200)
val toBeRemoved = Runnable {
executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
}
scheduler.postDelayed(toBeRemoved, 250)
scheduler.removeDelayedRunnable(toBeRemoved)
scheduler.postDelayed(
{ executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs) }, 100)
scheduler.postDelayed({
executionTimes.add(SystemClock.elapsedRealtime() - initialTimeMs)
cv.open() }, 300)
}
cv.block(TIMEOUT_MS)
assertEquals(4, executionTimes.size)
assertThat(executionTimes[0]).isIn(Range.closed(0L, TOLERANCE_MS))
assertThat(executionTimes[1]).isIn(Range.closed(100L, 100 + TOLERANCE_MS))
assertThat(executionTimes[2]).isIn(Range.closed(200L, 200 + TOLERANCE_MS))
assertThat(executionTimes[3]).isIn(Range.closed(300L, 300 + TOLERANCE_MS))
} cleanup {
visibleOnHandlerThread(handler) { scheduler.close() }
}
}
@Test
fun testMultipleSendDelayedMessages() {
val scheduler = RealtimeScheduler(handler)
tryTest {
val MSG_ID_0 = 0
val MSG_ID_1 = 1
val MSG_ID_2 = 2
val MSG_ID_3 = 3
val MSG_ID_4 = 4
val initialTimeMs = SystemClock.elapsedRealtime()
val executionTimes = mutableListOf<Long>()
val cv = ConditionVariable()
handler.post {
scheduler.sendDelayedMessage(
Message.obtain(handler, MSG_ID_0, Pair(ConditionVariable(), executionTimes)), 0)
scheduler.sendDelayedMessage(
Message.obtain(handler, MSG_ID_1, Pair(ConditionVariable(), executionTimes)),
200)
scheduler.sendDelayedMessage(
Message.obtain(handler, MSG_ID_4, Pair(ConditionVariable(), executionTimes)),
250)
scheduler.removeDelayedMessage(MSG_ID_4)
scheduler.sendDelayedMessage(
Message.obtain(handler, MSG_ID_2, Pair(ConditionVariable(), executionTimes)),
100)
scheduler.sendDelayedMessage(
Message.obtain(handler, MSG_ID_3, Pair(cv, executionTimes)),
300)
}
cv.block(TIMEOUT_MS)
assertEquals(4, executionTimes.size)
assertThat(executionTimes[0] - initialTimeMs).isIn(Range.closed(0L, TOLERANCE_MS))
assertThat(executionTimes[1] - initialTimeMs)
.isIn(Range.closed(100L, 100 + TOLERANCE_MS))
assertThat(executionTimes[2] - initialTimeMs)
.isIn(Range.closed(200L, 200 + TOLERANCE_MS))
assertThat(executionTimes[3] - initialTimeMs)
.isIn(Range.closed(300L, 300 + TOLERANCE_MS))
} cleanup {
visibleOnHandlerThread(handler) { scheduler.close() }
}
}
}

View file

@ -1,114 +0,0 @@
/*
* Copyright (C) 2024 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.net.module.util
import android.os.Build
import android.os.ConditionVariable
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import android.os.Message
import androidx.test.filters.SmallTest
import com.android.net.module.util.TimerFileDescriptor.ITask
import com.android.net.module.util.TimerFileDescriptor.MessageTask
import com.android.net.module.util.TimerFileDescriptor.RunnableTask
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.tryTest
import com.android.testutils.visibleOnHandlerThread
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import java.time.Duration
import java.time.Instant
import kotlin.test.assertFalse
import kotlin.test.assertTrue
private const val MSG_TEST = 1
@DevSdkIgnoreRunner.MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
class TimerFileDescriptorTest {
private class TestHandler(looper: Looper) : Handler(looper) {
override fun handleMessage(msg: Message) {
val cv = msg.obj as ConditionVariable
cv.open()
}
}
private val thread = HandlerThread(TimerFileDescriptorTest::class.simpleName).apply { start() }
private val handler by lazy { TestHandler(thread.looper) }
@After
fun tearDown() {
thread.quitSafely()
thread.join()
}
private fun assertDelayedTaskPost(
timerFd: TimerFileDescriptor,
task: ITask,
cv: ConditionVariable
) {
val delayTime = 10L
val startTime1 = Instant.now()
handler.post { timerFd.setDelayedTask(task, delayTime) }
assertTrue(cv.block(100L /* timeoutMs*/))
assertTrue(Duration.between(startTime1, Instant.now()).toMillis() >= delayTime)
}
@Test
fun testSetDelayedTask() {
val timerFd = TimerFileDescriptor(handler)
tryTest {
// Verify the delayed task is executed with the self-implemented ITask
val cv1 = ConditionVariable()
assertDelayedTaskPost(timerFd, { cv1.open() }, cv1)
// Verify the delayed task is executed with the RunnableTask
val cv2 = ConditionVariable()
assertDelayedTaskPost(timerFd, RunnableTask{ cv2.open() }, cv2)
// Verify the delayed task is executed with the MessageTask
val cv3 = ConditionVariable()
assertDelayedTaskPost(timerFd, MessageTask(handler.obtainMessage(MSG_TEST, cv3)), cv3)
} cleanup {
visibleOnHandlerThread(handler) { timerFd.close() }
}
}
@Test
fun testCancelTask() {
// The task is posted and canceled within the same handler loop, so the short delay used
// here won't cause flakes.
val delayTime = 10L
val timerFd = TimerFileDescriptor(handler)
val cv = ConditionVariable()
tryTest {
handler.post {
timerFd.setDelayedTask({ cv.open() }, delayTime)
assertTrue(timerFd.hasDelayedTask())
timerFd.cancelTask()
assertFalse(timerFd.hasDelayedTask())
}
assertFalse(cv.block(20L /* timeoutMs*/))
} cleanup {
visibleOnHandlerThread(handler) { timerFd.close() }
}
}
}