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

Async diffing and model building #425

Merged
merged 7 commits into from
Aug 7, 2018
Merged

Async diffing and model building #425

merged 7 commits into from
Aug 7, 2018

Conversation

elihart
Copy link
Contributor

@elihart elihart commented Apr 23, 2018

First pass at #286

cc @ngsilverman @gpeal

Copy link
Contributor

@ngsilverman ngsilverman left a comment

Choose a reason for hiding this comment

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

LGTM!

*
* Also adds support for canceling an in progress diff.
*/
class AsyncListDifferWithPayload<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add a @UiThread annotation to the whole class?

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 actually want to figure out if we should support setting a new list from any thread. That could be nice when the models are built on a background thread. It would complicate this to synchronize things though. cc @gpeal

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the most important thing is that it happens off of the main thread rather than on a specific executor

@@ -10,6 +10,10 @@
private List<? extends EpoxyModel<?>> currentModels;
private boolean insideSetModels;

public SimpleEpoxyController() {
setDebugLoggingEnabled(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, why make this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this was just for debugging purposes, I should have removed it.

thanks for the catch!

Copy link
Collaborator

@gpeal gpeal left a comment

Choose a reason for hiding this comment

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

Exciting!

*
* Also adds support for canceling an in progress diff.
*/
class AsyncListDifferWithPayload<T> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the most important thing is that it happens off of the main thread rather than on a specific executor


final DiffCallback<T> wrappedCallback = new DiffCallback<>(list, newList, diffCallback);

DIFF_EXECUTOR.execute(new Runnable() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This would be easier with coroutines :)

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 :( not ready to add that dependency yet though

@elihart elihart mentioned this pull request May 29, 2018
@elihart
Copy link
Contributor Author

elihart commented Jun 16, 2018

There are two things left I need to address:

Update documentation to reflect that duplicate unique ids won't crash anymore. I could build debug handling to check this case however.

Right now some people rely on posting a runnable after requesting a model build to make sure something runs after item changes are notified (such as scrolling to the top of the list after an item is inserted). This will no longer work, so will be a breaking change. I'd like to make sure I have a good alternative to present, and possibly enable this conditionally to give people time to update

@appsailor
Copy link

@elihart , any idea when this is getting released? Looking forward to trying it out on my upcoming android app which uses Firebase real time db. thx Aldrin

@elihart elihart force-pushed the eli-async_diffing branch from 196bd54 to 05ede30 Compare August 6, 2018 00:44
@elihart
Copy link
Contributor Author

elihart commented Aug 6, 2018

@ngsilverman @gpeal @BenSchwab This is updated to make the differ completely thread safe (it can now handle read/write from any thread), and also adds support for async model building.

This is my approach:

EpoxyController has a second constructor that takes two `Handler` instances, one for running model building and one for handling diffing. By default these use the main thread, but they can be changed to allow async work.

The default handlers can be changed by setting the static fields `EpoxyController#defaultDiffingHandler` and `EpoxyController#defaultModelBuildingHandler` which lets you force async behavior for all controllers in an app.

For example, you can set the global default to async like this
    HandlerThread handlerThread = new HandlerThread("epoxy");
    handlerThread.start();
    Handler handler = new Handler(handlerThread.getLooper());
    EpoxyController.defaultDiffingHandler = handler;
   EpoxyController.defaultModelBuildingHandler = handler;

A Handler is used because epoxy needs to be able to easily work with the main thread as the default. Also, Epoxy doesn't support parallel model building, and a Handler enforces this with it's single threaded looping behavior.

Note that if you use async model building you must guarantee that all of your data access is thread safe - Epoxy does not help you with that. This means that your `EpoxyController#buildModels` implementation is thread safe in all of it's data access.

If the same Handler is provided for model building and diffing then all of that work will proceed synchronously.

I feel pretty good about these changes, but it definitely adds a fair amount of complexity that is hard to test. I played around with it in the sample app and it seems good, but we'll want to test this carefully in airbnb too.

I took great care to make sure we could release this without changing user's model building behavior at all. Default behavior is still posted, but then runs synchronously until adapter changes are notified. This isn't too trivial, because by default the differ posts it's work to whatever thread it is given. I set up a wrapper class to execute Runnables asynchronously if its looper is the same. This is another reason I need to use Handlers instead of an Executor.

I also added a new listener that can be registered for when model build finishes.

Please review the concurrency logic carefully and lmk if you see room for improvement.

@elihart
Copy link
Contributor Author

elihart commented Aug 6, 2018

@appsailor sorry, I just saw your comment. The work is now done, and I'll be releasing this week.

Copy link
Collaborator

@gpeal gpeal left a comment

Choose a reason for hiding this comment

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

@elihart looks great overall. It's tough to fully grok the whole thing but overall, it looks good :) Just a couple of questions.

* Marks the generation as done, and updates the list if the generation is the most recent.
* Calls to this MUST be synchronized on "this" so list object and generation always stay in sync.
* This method isn't synchronized directly so callers have flexibility to includes other
* actions in their synchronized block
Copy link
Collaborator

Choose a reason for hiding this comment

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

Java uses reentrant locks for synchronized blocks. If you synchronize the method and call it from within another synchronized block, that should be fine.

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 makes a lot of sense, I should have looked that up :P

* @return True if the given generation is the most recent, in which case the given list was
* set. False if the generation is old and the list was ignored.
*/
private boolean tryLatchList(@Nullable List<? extends EpoxyModel<?>> newList, int runGeneration) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this @WorkerThread?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nope, anythread. annotated


if (newList == previousList) {
// nothing to do
onRunCompleted(runGeneration, newList, new DiffResult(previousList, newList, null));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe you can create static functions for these like:
DiffResult.noop(), DiffResult.clear(), and DiffResult.insert()

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 idea

@Nullable final DiffUtil.DiffResult differResult;

@SuppressWarnings("unchecked")
public DiffResult(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a javadoc for what null means?

@elihart elihart changed the title Async diffing Async diffing and model building Aug 7, 2018
@elihart elihart merged commit 5c51480 into master Aug 7, 2018
@elihart elihart deleted the eli-async_diffing branch August 7, 2018 16:17
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.

4 participants