diff --git a/packages/SystemUI/docs/executors.md b/packages/SystemUI/docs/executors.md new file mode 100644 index 0000000000000..8520ce228c9d3 --- /dev/null +++ b/packages/SystemUI/docs/executors.md @@ -0,0 +1,321 @@ +# Executors + +go/sysui-executors + +[TOC] + +## TLDR + +In SystemUI, we are encouraging the use of Java's [Executor][Executor] over +Android's [Handler][Handler] when shuffling a [Runnable][Runnable] between +threads or delaying the execution of a Runnable. We have an implementation of +Executor available, as well as our own sub-interface, +[DelayableExecutor][DelayableExecutor] available. For test, +[FakeExecutor][FakeExecutor] is available. + +[Executor]: https://developer.android.com/reference/java/util/concurrent/Executor.html +[Handler]: https://developer.android.com/reference/android/os/Handler +[Runnable]: https://developer.android.com/reference/java/lang/Runnable.html +[DelayableExecutor]: /packages/SystemUI/src/com/android/systemui/util/concurrency/DelayableExecutor.java +[FakeExecutor]: /packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java + +## Rationale + +Executors make testing easier and are generally more flexible than Handlers. +They are defined as an interface, making it easy to swap in fake implementations +for testing. This also makes it easier to supply alternate implementations +generally speaking - shared thread pools; priority queues; etc. + +For testing, whereas a handler involves trying to directly control its +underlying Looper (using things like `Thread.sleep()` as well as overriding +internal behaviors), an Executor implementation can be made to be directly +controllable and inspectable. + +See also go/executors-for-the-android-engineer + +## Available Executors + +At present, there are two interfaces of Executor avaiable, each implemented, and +each with two instances - `@Background` and `@Main`. + +### Executor + +The simplest Executor available implements the interface directly, making +available one method: `Executor.execute()`. You can access an implementation of +this Executor through Dependency Injection: + +```java + public class Foobar { + @Inject + public Foobar(@Background Executor bgExecutor) { + bgExecutor.execute(new Runnable() { + // ... + }); + } + } +``` + +`@Main` will give you an Executor that runs on the ui thread. `@Background` will +give you one that runs on a _shared_ non-ui thread. If you ask for an +non-annotated Executor, you will get the `@Background` Executor. + +We do not currently have support for creating an Executor on a new, virgin +thread. We do not currently support any sort of shared pooling of threads. If +you require either of these, please reach out. + +### DelayableExecutor + +[DelayableExecutor][DelayableExecutor] is the closest analogue we provide to +Handler. It adds `executeDelayed(Runnable r, long delayMillis)` and +`executeAtTime(Runnable r, long uptimeMillis)` to the interface, just like +Handler's [postDelayed][postDelayed] and [postAtTime][postAttime]. It also adds +the option to supply a [TimeUnit][TimeUnit] as a third argument. + +A DelayableExecutor can be accessed via Injection just like a standard Executor. +In fact, at this time, it shares the same underlying thread as our basic +Executor. + +```java + public class Foobar { + @Inject + public Foobar(@Background DelayableExecutor bgExecutor) { + bgExecutor.executeDelayed(new Runnable() { + // ... + }, 1, TimeUnit.MINUTES); + } + } +``` + +Unlike Handler, the added methods return a Runnable that, when run, cancels the +originally supplied Runnable if it has not yet started execution: + +```java + public class Foobar { + @Inject + public Foobar(@Background DelayableExecutor bgExecutor) { + Runnable cancel = bgExecutor.executeDelayed(new Runnable() { + // ... + }, 1, TimeUnit.MINUTES); + + cancel.run(); // The supplied Runnable will (probably) not run. + } + } +``` + +[postDelayed]: https://developer.android.com/reference/android/os/Handler#postDelayed(java.lang.Runnable,%20long) +[postAttime]: https://developer.android.com/reference/android/os/Handler#postAtTime(java.lang.Runnable,%20long) +[TimeUnit]: https://developer.android.com/reference/java/util/concurrent/TimeUnit + +## Moving From Handler + +Most use cases of Handlers can easily be handled by the above two interfaces +above. A minor refactor makes the switch: + +Handler | Executor | DelayableExecutor +------------- | --------- | ----------------- +post() | execute() | execute() +postDelayed() | `none` | executeDelayed() +postAtTime() | `none` | executeAtTime() + +There is one notable gap in this implementation: `Handler.postAtFrontOfQueue()`. +If you require this method, or similar, please reach out. The idea of a +PriorityQueueExecutor has been floated, but will not be implemented until there +is a clear need. + +Note also that "canceling" semantics are different. Instead of passing a `token` +object to `Handler.postDelayed()`, you receive a Runnable that, when run, +cancels the originally supplied Runnable. + +### Message Handling + +Executors have no concept of message handling. This is an oft used feature of +Handlers. There are (as of 2019-12-05) 37 places where we subclass Handler to +take advantage of this. However, by-and-large, these subclases take the +following form: + +```Java +mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_A: + handleMessageA(); + break; + case MSG_B: + handleMessageB((String) msg.obj); + break; + case MSG_C: + handleMessageC((Foobar) msg.obj); + break; + // ... + } + } +}; + +// Elsewhere in the class +void doSomething() { + mHandler.obtainMessage(MSG_B, "some string"); + mHandler.sendMessage(msg); +} +``` + +This could easily be replaced by equivalent, more direct Executor code: + +```Java +void doSomething() { + mExecutor.execute(() -> handleMessageB("some string")); +} +``` + +If you are posting Runnables frequently and you worry that the cost of creating +anonymous Runnables is too high, consider creating pre-defined Runnables as +fields in your class. + +If you feel that you have a use case that this does not cover, please reach out. + +### Handlers Are Still Necessary + +Handlers aren't going away. There are Android APIs that still require them (even +if future API development discourages them). A simple example is +[ContentObserver][ContentObserver]. Use them where necessary. + +[ContentObserver]: https://developer.android.com/reference/android/database/ContentObserver + +## Testing (FakeExecutor) + +We have a [FakeExecutor][FakeExecutor] available. It implements +DelayableExecutor (which in turn is an Executor). It takes a FakeSystemClock in +its constructor that allows you to control the flow of time, executing supplied +Runnables in a deterministic manner. + +The implementation is well documented and tested. You are encouraged to read and +reference it, but here is a quick overview: + +
| Method | +Description | +
|---|---|
| execute() | ++ Queues a Runnable so that it is "ready" + to run. (A Runnable is "ready" when its + scheduled time is less than or equal to + the clock.) + | +
| postDelayed() & postAtTime() | ++ Queues a runnable to be run at some + point in the future. + | +
| runNextReady() | ++ Run one runnable if it is ready to run + according to the supplied clock. + | +
| runAllReady() | ++ Calls runNextReady() in a loop until + there are no more "ready" runnables. + | +
| advanceClockToNext() | ++ Move the internal clock to the item at + the front of the queue, making it + "ready". + | +
| advanceClockToLast() | ++ Makes all currently queued items ready. + | +
| numPending() | ++ The number of runnables waiting to be run + They are not necessarily "ready". + | +
| (static method) exhaustExecutors() | ++ Given a number of FakeExecutors, it + calls runAllReady() repeated on them + until none of them have ready work. + Useful if you have Executors that post + work to one another back and forth. + | +