blob: b525e09bcfbba16347a299d61db6e2bab8236a8d [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.base.task;
import android.os.Process;
import android.util.Pair;
import androidx.annotation.Nullable;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;
import org.chromium.base.TraceEvent;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.annotation.concurrent.GuardedBy;
/**
* Implementation of the abstract class {@link TaskRunnerImpl}. Uses AsyncTasks until
* native APIs are available.
*/
@JNINamespace("base")
public class TaskRunnerImpl implements TaskRunner {
// TaskRunnerCleaners are enqueued to this queue when their WeakReference to a TaskRunnerIml is
// cleared.
private static final ReferenceQueue<Object> sQueue = new ReferenceQueue<>();
// Holds a strong reference to the pending TaskRunnerCleaners so they don't get GC'd before the
// TaskRunnerImpl they're weakly referencing does.
@GuardedBy("sCleaners")
private static final Set<TaskRunnerCleaner> sCleaners = new HashSet<>();
private final @TaskTraits int mTaskTraits;
private final String mTraceEvent;
private final @TaskRunnerType int mTaskRunnerType;
// Volatile is sufficient for synchronization here since we never need to read-write and
// volatile makes writes to it immediately visible to other threads.
// When |mNativeTaskRunnerAndroid| is set, native has been initialized and pre-native tasks have
// been migrated to the native task runner.
private volatile long mNativeTaskRunnerAndroid;
protected final Runnable mRunPreNativeTaskClosure = this::runPreNativeTask;
private final Object mPreNativeTaskLock = new Object();
@GuardedBy("mPreNativeTaskLock")
private boolean mDidOneTimeInitialization;
@Nullable
@GuardedBy("mPreNativeTaskLock")
private LinkedList<Runnable> mPreNativeTasks;
@Nullable
@GuardedBy("mPreNativeTaskLock")
private List<Pair<Runnable, Long>> mPreNativeDelayedTasks;
int clearTaskQueueForTesting() {
int taskCount = 0;
synchronized (mPreNativeTaskLock) {
if (mPreNativeTasks != null) {
taskCount = mPreNativeTasks.size() + mPreNativeDelayedTasks.size();
mPreNativeTasks.clear();
mPreNativeDelayedTasks.clear();
}
}
return taskCount;
}
private static class TaskRunnerCleaner extends WeakReference<TaskRunnerImpl> {
final long mNativePtr;
TaskRunnerCleaner(TaskRunnerImpl runner) {
super(runner, sQueue);
mNativePtr = runner.mNativeTaskRunnerAndroid;
}
void destroy() {
TaskRunnerImplJni.get().destroy(mNativePtr);
}
}
/**
* The lifecycle for a TaskRunner is very complicated. Some task runners are static and never
* destroyed, some have a task posted to them and are immediately allowed to be GC'd by the
* creator, but if native isn't initialized the task would be lost if this were to be GC'd.
* This makes an explicit destroy impractical as it can't be enforced on static runners, and
* wouldn't actually destroy the runner before native initialization as that would cause tasks
* to be lost. A finalizer could give us the correct behaviour here, but finalizers are banned
* due to the performance cost they impose, and all of the many correctness gotchas around
* implementing a finalizer.
*
* The strategy we've gone with here is to use a ReferenceQueue to keep track of which
* TaskRunners are no longer reachable (and may have been GC'd), and to delete the native
* counterpart for those TaskRunners by polling the queue when doing non-performance-critical
* operations on TaskRunners, like creating a new one. In order to prevent this TaskRunner from
* being GC'd before its tasks can be posted to the native runner, PostTask holds a strong
* reference to each TaskRunner with a task posted to it before native initialization.
*/
private static void destroyGarbageCollectedTaskRunners() {
while (true) {
// ReferenceQueue#poll immediately removes and returns an element from the queue,
// returning null if the queue is empty.
@SuppressWarnings("unchecked")
TaskRunnerCleaner cleaner = (TaskRunnerCleaner) sQueue.poll();
if (cleaner == null) return;
cleaner.destroy();
synchronized (sCleaners) {
sCleaners.remove(cleaner);
}
}
}
/**
* @param traits The TaskTraits associated with this TaskRunnerImpl.
*/
TaskRunnerImpl(@TaskTraits int traits) {
this(traits, "TaskRunnerImpl", TaskRunnerType.BASE);
destroyGarbageCollectedTaskRunners();
}
/**
* @param traits The TaskTraits associated with this TaskRunnerImpl.
* @param traceCategory Specifies the name of this instance's subclass for logging purposes.
* @param taskRunnerType Specifies which subclass is this instance for initialising the correct
* native scheduler.
*/
protected TaskRunnerImpl(
@TaskTraits int traits, String traceCategory, @TaskRunnerType int taskRunnerType) {
mTaskTraits = traits;
mTraceEvent = traceCategory + ".PreNativeTask.run";
mTaskRunnerType = taskRunnerType;
}
@Override
public void postTask(Runnable task) {
postDelayedTask(task, 0);
}
@Override
public void postDelayedTask(Runnable task, long delay) {
// Lock-free path when native is initialized.
if (mNativeTaskRunnerAndroid != 0) {
TaskRunnerImplJni.get().postDelayedTask(
mNativeTaskRunnerAndroid, task, delay, task.getClass().getName());
return;
}
synchronized (mPreNativeTaskLock) {
oneTimeInitialization();
if (mNativeTaskRunnerAndroid != 0) {
TaskRunnerImplJni.get().postDelayedTask(
mNativeTaskRunnerAndroid, task, delay, task.getClass().getName());
return;
}
// We don't expect a whole lot of these, if that changes consider pooling them.
// If a task is scheduled for immediate execution, we post it on the
// pre-native task runner. Tasks scheduled to run with a delay will
// wait until the native task runner is initialised.
if (delay == 0) {
mPreNativeTasks.add(task);
schedulePreNativeTask();
} else if (!schedulePreNativeDelayedTask(task, delay)) {
Pair<Runnable, Long> preNativeDelayedTask = new Pair<>(task, delay);
mPreNativeDelayedTasks.add(preNativeDelayedTask);
}
}
}
protected Boolean belongsToCurrentThreadInternal() {
// TODO(https://crbug.com/1026641): This function shouldn't be here, and should only be used
// by derived classes (eg. SingleThreadTaskRunner) until it is moved there, as TaskRunner
// has no notion of belonging to a thread.
assert !getClass().equals(TaskRunnerImpl.class);
synchronized (mPreNativeTaskLock) {
oneTimeInitialization();
}
if (mNativeTaskRunnerAndroid == 0) return null;
return TaskRunnerImplJni.get().belongsToCurrentThread(mNativeTaskRunnerAndroid);
}
@GuardedBy("mPreNativeTaskLock")
private void oneTimeInitialization() {
if (mDidOneTimeInitialization) return;
mDidOneTimeInitialization = true;
if (!PostTask.registerPreNativeTaskRunner(this)) {
initNativeTaskRunner();
} else {
mPreNativeTasks = new LinkedList<>();
mPreNativeDelayedTasks = new ArrayList<>();
}
}
/**
* Must be overridden in subclasses, schedules a call to runPreNativeTask() at an appropriate
* time.
*/
protected void schedulePreNativeTask() {
PostTask.getPrenativeThreadPoolExecutor().execute(mRunPreNativeTaskClosure);
}
/**
* Overridden in subclasses that support Delayed tasks pre-native.
*
* @return true if the task has been scheduled and does not need to be forwarded to the native
* task runner.
*/
protected boolean schedulePreNativeDelayedTask(Runnable task, long delay) {
return false;
}
/**
* Runs a single task and returns when its finished.
*/
// The trace event name is derived from string literals.
@SuppressWarnings("NoDynamicStringsInTraceEventCheck")
protected void runPreNativeTask() {
try (TraceEvent te = TraceEvent.scoped(mTraceEvent)) {
Runnable task;
synchronized (mPreNativeTaskLock) {
if (mPreNativeTasks == null) return;
task = mPreNativeTasks.poll();
}
switch (mTaskTraits) {
case TaskTraits.BEST_EFFORT:
case TaskTraits.BEST_EFFORT_MAY_BLOCK:
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
break;
case TaskTraits.USER_VISIBLE:
case TaskTraits.USER_VISIBLE_MAY_BLOCK:
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
break;
case TaskTraits.USER_BLOCKING:
case TaskTraits.USER_BLOCKING_MAY_BLOCK:
Process.setThreadPriority(Process.THREAD_PRIORITY_MORE_FAVORABLE);
break;
// We don't want to lower the Thread Priority of the UI Thread, especially
// pre-native, as the Thread is oversubscribed, highly latency sensitive, and
// there's only a single task queue so low priority tasks can run ahead of high
// priority tasks.
case TaskTraits.UI_BEST_EFFORT: // Fall-through.
case TaskTraits.UI_USER_VISIBLE: // Fall-through.
case TaskTraits.UI_USER_BLOCKING: // Fall-through.
break;
// lint ensures all cases are checked.
}
task.run();
}
}
/**
* Instructs the TaskRunner to initialize the native TaskRunner and migrate any tasks over to
* it.
*/
/* package */ void initNativeTaskRunner() {
long nativeTaskRunnerAndroid = TaskRunnerImplJni.get().init(mTaskRunnerType, mTaskTraits);
synchronized (mPreNativeTaskLock) {
if (mPreNativeTasks != null) {
for (Runnable task : mPreNativeTasks) {
TaskRunnerImplJni.get().postDelayedTask(
nativeTaskRunnerAndroid, task, 0, task.getClass().getName());
}
mPreNativeTasks = null;
}
if (mPreNativeDelayedTasks != null) {
for (Pair<Runnable, Long> task : mPreNativeDelayedTasks) {
TaskRunnerImplJni.get().postDelayedTask(nativeTaskRunnerAndroid, task.first,
task.second, task.getClass().getName());
}
mPreNativeDelayedTasks = null;
}
// mNativeTaskRunnerAndroid is volatile and setting this indicates we've have migrated
// all pre-native tasks and are ready to use the native Task Runner.
assert mNativeTaskRunnerAndroid == 0;
mNativeTaskRunnerAndroid = nativeTaskRunnerAndroid;
}
synchronized (sCleaners) {
sCleaners.add(new TaskRunnerCleaner(this));
}
// Destroying GC'd task runners here isn't strictly necessary, but the performance of
// initNativeTaskRunner() isn't critical, and calling the function more often will help
// prevent any potential build-up of orphaned native task runners.
destroyGarbageCollectedTaskRunners();
}
@NativeMethods
interface Natives {
long init(@TaskRunnerType int taskRunnerType, @TaskTraits int taskTraits);
void destroy(long nativeTaskRunnerAndroid);
void postDelayedTask(
long nativeTaskRunnerAndroid, Runnable task, long delay, String runnableClassName);
boolean belongsToCurrentThread(long nativeTaskRunnerAndroid);
}
}