Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal/rehash: Use async Messaging #416

Closed
wants to merge 21 commits into from

Conversation

ZacSweers
Copy link
Contributor

@ZacSweers ZacSweers commented Feb 12, 2018

This PR changes the default main thread Handler to use asynchronous messaging. This allows us to avoid VSYNC locking that otherwise pushes every post to the next frame.

This works by relying on the new Handler.createAsync factory in API 28, and on pre-28 it will reflectively fall back to a private constructor of Handler (that's always existed) to enable this.

This should give us the original goal of #228 with the safety of still leaning on the main thread looper and not trying to directly run things ourselves and risking deadlocks/trampolining issues.

To use SDK 28, updated the AGP plugin and gradle along the way.

Prior discussion below

This is a main thread scheduler that's optimized to directly execute given Runnables if it's already on the main thread, rather than always scheduling.

Prior art: #228

Kept as a standalone implementation right now for review and discussion, but ideally this implementation would replace the existing main thread scheduler implementation as the default.

This is a main thread scheduler that's optimized to directly execute given runnables if it's already on the main thread, rather than always scheduling.
@ZacSweers
Copy link
Contributor Author

Note that this is upstreaming from our internal implementation at Uber. We've been using this in our production apps for a little over a year now.

From some discussion with @JakeWharton offline: does it make sense for a scheduler that doesn't always necessarily "schedule"? Some potential precedent of this in RxJava include TrampolineScheduler's implementation and ImmediateThinScheduler. @akarnokd would really love to get your thoughts on this!

@akarnokd
Copy link
Member

I think I expressed my views in the prior art. Doing it plainly may result in a stack overflow, adding a main-thread trampoline may add overhead negating the optimization and supporting task disposal may become quite complicated.

@ZacSweers
Copy link
Contributor Author

Agreed the main thread trampoline adds a lot of overhead. Is it really necessary though? It seems like that risks duplicating what the main thread looper does under the hood.

For stack overflow, is that due to stacktraces no longer being chopped? I didn't quite follow that bit

@akarnokd
Copy link
Member

akarnokd commented Feb 12, 2018

Worker w = new FastPathScheduler().createWorker();

Schedulers.single().scheduleDirect(() -> w.dispose(), 10, TimeUnit.SECONDS);

w.schedule(new Runnable() {
    @Override public void run() {
        w.schedule(this);
    } 
});

Thread.sleep(11000);

Trampoline would limit the stack depth.

@damianw
Copy link

damianw commented Apr 17, 2018

Doesn't this potentially violate the serial requirement for schedulers? If you have an upstream observable that emits a value which must be scheduled on the handler, followed closely by an emission which can be immediate (or trampolined), the second emission could easily pass through to the subscriber before the first.

My first thought for solving this problem would be to only take the fast path when there are no queued emissions.

@ZacSweers ZacSweers changed the title Proposal/rehash: Implement FastPathScheduler Proposal/rehash: Use an async Handler Jun 26, 2018
@ZacSweers
Copy link
Contributor Author

Hey all - after a lot of discussion offline and with the Android framework team, I've updated the PR with a new solution that relies on asynchronous Messages to avoid vsync locking while keeping the safety of the main thread looper. I've updated the description as well, please take a look!

@ZacSweers
Copy link
Contributor Author

ZacSweers commented Jun 26, 2018

I hate Travis CI sometimes. Will debug more after allowing time for discussion

@@ -47,6 +48,28 @@ public static Scheduler from(Looper looper) {
return new HandlerScheduler(new Handler(looper));
}

@TargetApi(Build.VERSION_CODES.P)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree! But lint fails without it despite the if-check :|

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you try just using 28 as a literal? I prefer avoiding the codenames (which are thankfully almost dead) and letters (which we should be trying to kill next).

Copy link
Contributor Author

@ZacSweers ZacSweers Jun 28, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, yeah using just 28 worked. c9e2bbf

RIP dessert club

.getConstructor(Looper.class, Handler.Callback.class, boolean.class)
.newInstance(looper, null, true);
} catch (IllegalAccessException e) {
return new Handler(looper);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just make these all empty and hoist a single return outside.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 551b7b4

private static Handler createAsyncHandler(Looper looper) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return Handler.createAsync(looper);
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else not required after return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


static final Scheduler DEFAULT = new HandlerScheduler(new Handler(Looper.getMainLooper()));
static final Scheduler DEFAULT
= new HandlerScheduler(createAsyncHandler(Looper.getMainLooper()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it's going to require a major version update. The implications to the entirety of the world which uses RxAndroid are scary and a silent change in a minor release doesn't seem appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but a major change would require breaking with keeping the same major version as rxjava :|.

Either that or maybe do an extended preview release and ask for feedback?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we overload from(Looper) with a version that takes a boolean for async just like the Handler constructor no matter what.

As to getting it into mainThread(), you can use the RxAndroidPlugins to install an init callback for its creation which you can then delegate to from(Looper.getMainLooper(), true).

I think that's the minimum-viable API to at least get this into a release now for people to start testing it. And from there we can determine whether or not it's even possible to make it the default behind mainThread() in the next major version–or sometime in the future–and whether or not we need to expose a vsyncMainThread() or similar accessor to a singleton instance for code which truly needs the old behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works too! For some reason I had it in my head that you were against having support for both. Can update. Should I add a brief mention/example in the readme with it or save that for changelog?

@JakeWharton
Copy link
Member

Can we get rid of the use of Handler and just send raw Message instances to the Looper? Then we can use Message.setAsynchronous(boolean) which doesn't require reflection?

@ZacSweers
Copy link
Contributor Author

ZacSweers commented Jun 29, 2018

We could but that API was only added (made public?) on API 22. If it was just private before we'd still need reflection.

@JakeWharton
Copy link
Member

@ZacSweers
Copy link
Contributor Author

Oh wtf. I could have sworn I went back and saw the handler constructor on older APIs (my understanding was it existed with the introduction of project butter circa API 17). In that case, yeah we can avoid reflection. Probably doesn't make sense to use the new createAsync API on 28+ either to keep things simple, but it'd almost certainly be a breaking change or a new separate scheduler that has the Message-based API. How would you want that to look?

@JakeWharton
Copy link
Member

We already use Message. Looks like all you need to do is keep a boolean in HandlerScheduler and set it here:

Message message = Message.obtain(handler, scheduled);
message.obj = this; // Used as token for batch disposal of this worker's runnables.
handler.sendMessageDelayed(message, unit.toMillis(delay));

@ZacSweers
Copy link
Contributor Author

So keep the handler then? I was thrown off by

Can we get rid of the use of Handler

@ZacSweers
Copy link
Contributor Author

ZacSweers commented Jun 29, 2018

We'd also have to promote HandlerScheduler to be a public API if you want it to be optional and usable via plugin, which it isn't right now

@JakeWharton
Copy link
Member

JakeWharton commented Jun 29, 2018 via email

@adamp
Copy link

adamp commented Jul 1, 2018

Hi, chiming in from the Android team to give a general thumbs up to skipping the vsync barriers here. Ordering between "async" messages is still preserved; we made waiting until after the barriers the default behavior back in Jellybean because there's a bunch of UI code out there that expects a message or runnable to be processed after any pending view tree traversals (measure/layout/draw) which wait for vsync after Jellybean. The tradeoff for the compatibility is a lot of latency that most use cases shouldn't have to pay for, which this proposal addresses.

The Message.setAsynchronous method has been in the codebase since API 16 (jellybean - https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/os/Message.java#397) and Handler's constructor with the boolean async param has been there since API 17 (jellybean-mr1 - https://android.googlesource.com/platform/frameworks/base/+/jb-mr1-release/core/java/android/os/Handler.java). It would be safe to opportunistically call either one of these using reflection (or for Message.setAsynchronous just by building against a newer sdk and using a different API level check/catch if something goes wrong) as of these API levels, provided that when you're running on a platform version where it's officially available you call the real public API. This is the policy we use for reflection in the Android support libraries - as long as the implementation is public API clean as of some published API level the use of reflection isn't defining de facto public API that we have to support into the future.

useAsync = false;
}
}
return new HandlerScheduler(new Handler(looper), useAsync);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already created the Handler above. Can pass it here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

derp, 6fbdff3

message.setAsynchronous(true);
} catch (NoSuchMethodError e) {
useAsync = false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should recycle the Message to be a good citizen!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ZacSweers
Copy link
Contributor Author

Alright, reverted the sdk 28 changes to punt the CI issues for now and also tweaked it to only do the setAsynchronous method check on API < 22. This shooooould be good to go now

@ZacSweers ZacSweers changed the title Proposal/rehash: Use an async Handler Proposal/rehash: Use async Messaging Jul 9, 2018
* locking. On API < 16, this will no-op.
* @see android.os.Message#setAsynchronous(boolean)
*/
@SuppressLint("NewApi")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still needed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess because the conditional is reversed it is

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah :/

*
* @param async if true, the scheduler will use async messaging on API >= 16 to avoid VSYNC
* locking. On API < 16, this will no-op.
* @see android.os.Message#setAsynchronous(boolean)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't need to specify package since this is now imported

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JakeWharton
Copy link
Member

Merged as 191a758

@ahulyk
Copy link

ahulyk commented Aug 19, 2018

to try that feature i need to init it in the Application onCreate() method, right?

AndroidPlugins.setInitMainThreadSchedulerHandler {
    AndroidSchedulers.from(Looper.getMainLooper(), true)
}

@limpep
Copy link

limpep commented Aug 20, 2018

@ahulyk here is an article on how to use it, https://medium.com/@sweers/rxandroids-new-async-api-4ab5b3ad3e93

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants