Skip to content

rxjava-android Poposal: Call action immediately in HandlerThreadScheduler if thread is the same #1102

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

Closed

Conversation

Yarikx
Copy link
Contributor

@Yarikx Yarikx commented Apr 25, 2014

By avoiding unnecessary Runnable creation, and posting it to handler we can decrease overhead when scheduling is made from the same thread.

Current Activity.runOnUIThread is implemented in the same way:

    public final void runOnUiThread(Runnable action) {
        if (Thread.currentThread() != mUiThread) {
            mHandler.post(action);
        } else {
            action.run();
        }
    }

if action is scheduled in current looper thread than just invoke Action instead of posting it as Runnable
@cloudbees-pull-request-builder

RxJava-pull-requests #1013 SUCCESS
This pull request looks good

@benjchristensen
Copy link
Member

This looks good to me ... but since I don't use Android, can others who do comment on this?

/cc @mttkay @zsxwing

@DylanSale
Copy link

Looks good to me. Great idea, I was thinking to add this as well. I wonder if it should be optional however, sometimes you need to be sure to post a runnable (if modifying certain view properties during a layout pass for example).

@zsxwing
Copy link
Member

zsxwing commented Apr 30, 2014

We have already a discussion here: #949

Activity.runOnUIThread is only used in the non-recursive case. However, for HandlerThreadScheduler, we want to support both recursive and non-recursive schedule.

@zsxwing
Copy link
Member

zsxwing commented Apr 30, 2014

Maybe adding an option to the HandlerThreadScheduler constructor to indicate if it supports recursive schedule?

But it would require the user learning the implementations of operators. They needs to be aware of which operator will use recursive schedule. I'm not convinced by this idea.

@Yarikx
Copy link
Contributor Author

Yarikx commented Apr 30, 2014

Yes, the main goal of this PR is to make mainThreadScheduler works like Activity.runOnUIThread, as in most cases it not require recursion. On the other hand sometimes user may want to use HandlerThreadScheduler for recursive tasks, and other long running tasks.

So may be it will be better to add option (as @zsxwing suggest) to constructor, but leave existed constructor (with single Handler argument) as default recursive implementation (so it will have the same behavior as now).
But make mainThreadScheduler non-recursie by default. So if the user do not want to worry about implementation, he can use handlerThreadScheduler as before.

@mttkay
Copy link
Contributor

mttkay commented Apr 30, 2014

I remember there was a PR before which was pretty much the same. @zsxwing can you elaborate on why it doesn't work with recursive scheduling? In which cases do you use recursive scheduling? Unless I'm simply not aware of it, I don't think I ever had the need for it on Android. Maybe we can find a compromise, because I generally agree that the extra roundtrip through the main looper is unnecessary overhead.

@zsxwing
Copy link
Member

zsxwing commented Apr 30, 2014

@mattkay, the PR is #949

My comments are here: #949 (comment) and #949 (comment)

@zsxwing
Copy link
Member

zsxwing commented Apr 30, 2014

@mattkay, and your comment: #949 (comment)

it will allow to avoid rescheduling actions if scheduled from the same thread
mostly usefull for mainThreadScheduler
@Yarikx
Copy link
Contributor Author

Yarikx commented Apr 30, 2014

How about this approach?
Use immediate way for mainThread by default,
Use usual one for default HandlerThreadScheduler

@cloudbees-pull-request-builder

RxJava-pull-requests #1052 FAILURE
Looks like there's a problem with this pull request

@mttkay
Copy link
Contributor

mttkay commented May 1, 2014

@zsxwing Could you elaborate again on a case where you need recursive scheduling? I'm not sure I understand when it's useful. I'm afraid we use a very simple, formulaic approach on Android, which is putting something on a background thread and observing it on the main thread. Is this used when rescheduling something as part of an observable transformation?

In any event, I think we should get some numbers to back up that it actually improves performance. We should measure what the exact performance gains are on at least a few devices and a varying numbers of events.

@zsxwing
Copy link
Member

zsxwing commented May 4, 2014

Could you elaborate again on a case where you need recursive scheduling?

Now looks only repeat and retry need recursive scheduling. However, if the count parameter is small, StackOverflowError and "Application Not Responding" should not happen.

@Yarikx, do you have some number to prove the performance improvement. Maybe this is a premature optimization.

@Yarikx
Copy link
Contributor Author

Yarikx commented May 5, 2014

Sorry for delay.
@zsxwing ok, I had some metrics before, but i will try to make new ones.

@Yarikx
Copy link
Contributor Author

Yarikx commented May 8, 2014

Ok, I check some metrics. I don't sure that the measurement is done right, but it's enough to make some assumptions.
All metrics was taken from Nexus S. Not the fastest phone now, but it's not the slowest ether.

So, I created two instances of HandlerThreadScheduler (let's call them immediate and recursive)

Handler handler = new Handler();
        Scheduler immediate = AndroidSchedulers.handlerThread(handler, true);
        Scheduler recursive = AndroidSchedulers.handlerThread(handler, false);

The good part

in this part i tested minimal delay for scheduling one item. This action was made in button handler, (after Activity was created, and all UI was initialized and Handler queue was empty)

    public void test(View view){
        Handler handler = new Handler();
        Scheduler immediate = AndroidSchedulers.handlerThread(handler, true);
        Scheduler recursive = AndroidSchedulers.handlerThread(handler, false);

        Observable.from(1)
                .timestamp()
                .observeOn(immediate)
                .subscribe(new Action1<Timestamped<Integer>>() {
                    @Override
                    public void call(Timestamped<Integer> timestamped) {
                        long time = System.currentTimeMillis() - timestamped.getTimestampMillis();
                        Log.d("Immediate", "time = "+time);
                    }
                });
        Observable.from(1)
                .timestamp()
                .observeOn(recursive)
                .subscribe(new Action1<Timestamped<Integer>>() {
                    @Override
                    public void call(Timestamped<Integer> timestamped) {
                        long time = System.currentTimeMillis() - timestamped.getTimestampMillis();
                        Log.d("Recursive", "time = "+time);
                    }
                });
    }

and the output after few button clicks:

05-08 11:36:47.632  17468-17468/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:36:47.632  17468-17468/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 1
05-08 11:36:48.937  17468-17468/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:36:48.937  17468-17468/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 1
05-08 11:36:50.007  17468-17468/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:36:50.007  17468-17468/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 0
05-08 11:36:50.914  17468-17468/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:36:50.914  17468-17468/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 1

So, actual gap between sending Runnable to Handler is not so much to worry about.
But, this is very simple example, as we can start to execute posted task right after button handler.

The bad part

Now let's try to process some sequence

    public void test(View view){
        Handler handler = new Handler();
        Scheduler immediate = AndroidSchedulers.handlerThread(handler, true);
        Scheduler recursive = AndroidSchedulers.handlerThread(handler, false);

        Observable.range(0, 20)
                .timestamp()
                .observeOn(immediate)
                .subscribe(new Action1<Timestamped<Integer>>() {
                    @Override
                    public void call(Timestamped<Integer> timestamped) {
                        long time = System.currentTimeMillis() - timestamped.getTimestampMillis();
                        Log.d("Immediate", "time = "+time);
                    }
                });
        Observable.range(0, 20)
                .timestamp()
                .observeOn(recursive)
                .subscribe(new Action1<Timestamped<Integer>>() {
                    @Override
                    public void call(Timestamped<Integer> timestamped) {
                        long time = System.currentTimeMillis() - timestamped.getTimestampMillis();
                        Log.d("Recursive", "time = "+time);
                    }
                });
    }

And the test result:

05-08 11:31:11.460  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.460  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.460  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.460  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.460  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 1
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 1
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.468  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.476  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.476  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 1
05-08 11:31:11.476  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.476  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.476  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.476  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.476  17324-17324/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:31:11.484  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 9
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 9
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 10
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 12
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 13
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 12
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 13
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 11
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 12
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 12
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 12
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 13
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 13
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 14
05-08 11:31:11.492  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 13
05-08 11:31:11.500  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 14
05-08 11:31:11.500  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 14
05-08 11:31:11.500  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 15
05-08 11:31:11.500  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 14
05-08 11:31:11.500  17324-17324/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 15

Now we see that first item processing (on recursive scheduler) start only after finishing current task (emitting items to scheduler). Actually in current example there is not so much work to do, but in real apps there can be more UI related work, so sequence observing will delay even more.

Another thing: As we process each item in it's own Runnable we see that "processing gap" grows linearly as sequence grows (because each item processing adds 0.5~2 ms)

The ugly part

Let's do last example, but add few more layers of scheduling, and we will run it on onCreate method to simulate case, when user have some more work to do, and when Handler queue is not empty

        Observable.range(0, 20)
                .timestamp()
                .observeOn(immediate)
                .observeOn(immediate)
                .observeOn(immediate)
                .observeOn(immediate)
                .observeOn(immediate)
                .observeOn(immediate)
                .observeOn(immediate)
                .subscribe(new Action1<Timestamped<Integer>>() {
                    @Override
                    public void call(Timestamped<Integer> timestamped) {
                        long time = System.currentTimeMillis() - timestamped.getTimestampMillis();
                        Log.d("Immediate", "time = "+time);
                    }
                });
        Observable.range(0, 20)
                .timestamp()
                .observeOn(recursive)
                .observeOn(recursive)
                .observeOn(recursive)
                .observeOn(recursive)
                .observeOn(recursive)
                .observeOn(recursive)
                .observeOn(recursive)
                .subscribe(new Action1<Timestamped<Integer>>() {
                    @Override
                    public void call(Timestamped<Integer> timestamped) {
                        long time = System.currentTimeMillis() - timestamped.getTimestampMillis();
                        Log.d("Recursive", "time = "+time);
                    }
                });

and the output:

05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 1
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 1
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 1
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.054  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 1
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.062  17896-17896/com.example.rxjavaperformancetest.app D/Immediate﹕ time = 0
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 222
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 222
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 223
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 223
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 224
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 224
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 224
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 225
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 225
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 226
05-08 11:53:15.289  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 226
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 227
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 227
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 227
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 227
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 228
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 228
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 228
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 228
05-08 11:53:15.296  17896-17896/com.example.rxjavaperformancetest.app D/Recursive﹕ time = 229

So we see here huge gap, enough to be noticed by the user in UI.

Conclusion

Of course immediate way of handler thread scheduling is more dangerous because we can get StackOverflowException in some cases. But usually work, scheduled in main thread is usually simple and just manipulate UI elements.

But for cases when we want to receive some expensive data, that can be cached (api call or bitmap), immediate way can be very helpful.

@zsxwing
Copy link
Member

zsxwing commented May 10, 2014

Sorry for the late reply. Thank you for your work, @Yarikx
However, looks your test is unfair.

  • Firstly, you are measuring the time interval between each action and the subscribe method.
  • Secondly, you are using the same handler. So the actions using recursive need to run after the actions using immediate. It means that the first time interval of recursive is whole time of immediate + action time interval.

Could you redo your test by timeInterval and run the test for immediate and recursive separately. E.g.,

        Observable.range(0, 20)
                .observeOn(immediate)
                .timeInterval(immediate)
                .subscribe(new Action1<TimeInterval<Integer>>() {
                    @Override
                    public void call(TimeInterval<Integer> interval) {
                        Log.d("Immediate", "time = " + interval.getIntervalInMilliseconds());
                    }
                });

@zsxwing
Copy link
Member

zsxwing commented May 10, 2014

In addition, make sure subscribe also runs in the handler. So I suppose the code should be

    public void testImmediate() {
        Handler handler = new Handler();
        final Scheduler immediate = AndroidSchedulers.handlerThread(handler, true);

        immediate.createWorker().schedule(new Action0() {
            @Override
            public void call() {
                Observable.range(0, 20)
                        .observeOn(immediate)
                        .timeInterval(immediate)
                        .subscribe(new Action1<TimeInterval<Integer>>() {
                            @Override
                            public void call(TimeInterval<Integer> interval) {
                                Log.d("Immediate", "time = " + interval.getIntervalInMilliseconds());
                            }
                        });
            }
        });
    }

    public void testRecursive() {
        Handler handler = new Handler();
        final Scheduler recursive = AndroidSchedulers.handlerThread(handler, false);

        recursive.createWorker().schedule(new Action0() {
            @Override
            public void call() {
                Observable.range(0, 20)
                        .observeOn(recursive)
                        .timeInterval(recursive)
                        .subscribe(new Action1<TimeInterval<Integer>>() {
                            @Override
                            public void call(TimeInterval<Integer> interval) {
                                Log.d("Recursive", "time = " + interval.getIntervalInMilliseconds());
                            }
                        });
            }
        });
    }

@mttkay
Copy link
Contributor

mttkay commented May 17, 2014

In general I'd be very careful with such manual measurements. Have a look at this document outlining how deceiving micro benchmarks can be: http://www.ibm.com/developerworks/java/library/j-jtp02225/index.html

The lack of authoritative documentation for Dalvik and the optimizations it applies makes that even worse.

Another thing I'm worried about is that introducing this change will in fact change the behavior of client code in some cases, so we might see application code failing. Here's why:

Say in one of your fragments, you create an observable and connect it in onCreate to a subscriber that fills the results into a view. With the current implementation this works: since observeOn(mainThread()) will post the callbacks to the message loop, you're guaranteed to not receive them before the fragment has been fully constructed, i.e. they will not arrive until after onViewCreated is called which is where you initialize your views. With this optimization in place, however, the callbacks become synchronous when executed from the main thread, so if the Observable was scheduled to run on the main thread, you will now get the call back immediately, i.e. before your views are fully initialized. As a result your application will crash.

Given, connecting a synchronous observable in fragment onCreate is not very likely. I'm just saying, these are things that have to be considered.

@mttkay
Copy link
Contributor

mttkay commented May 17, 2014

In fact, after some consideration, I'm against applying this patch. I think of observeOn as sending a message through the given scheduler, and if it's documented that sending a message this way always goes through Android's message loop, then as a caller we get predictable behavior and can rely on the guarantees Android gives us w.r.t. how these messages are processed, such as treating a rotation change of an Activity as an atomic operation from the perspective of message processing.

In other words, we're still fulfilling Android's message loop contract, since we're merely adapting its API to fulfill RxJava's Scheduler contract in terms of an Android message loop. By applying optimizations like these, we're bypassing the message loop and create non-deterministic behavior where sometimes the message loop is involved, sometimes it isn't. This will make it difficult to reason about failure on the call site unless this behavior is clearly documented.

Perhaps there is a way to make this explicit in the type system or in the Scheduler APIs? Another option would be to introduce a variant of HandlerThreadScheduler which performs such optimizations, only using the message loop when necessary, and treating a scheduled action as a synchronous message call otherwise.

@Yarikx
Copy link
Contributor Author

Yarikx commented May 17, 2014

@mttkay I think you are right. After some thoughts I also don't think this is the best way to do operations on handler.

I still think that there is better way to deal with main thread, but it's not here right now =)

So I will close this PR.

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.

6 participants