Live Query blocking UI thread

Hi,

We are currently using Couchbase 2.1.2 in our Android application.
We have around 70k documents in our app.

We have 5 live queries running in our app.

We know that whenever there will be db change notification, the query runs again, it compares previous results to the new results, and if there is a difference in the result set, it gives a notification.

What’s currently happening is that whenever we are getting frequent changes from our server, the UI thread of the application seems to get blocked. We are not currently passing the optional Executor parameter while adding the QueryChangeListener.

We wanted to confirm that the above process of comparing previous results to the new one, will that take place on the UI thread as well. We looked at the source code and it looks like it uses a SingleThreadScheduledExecutor to compare the previous results to the new one. We wanted to confirm if that was right, and if it is can you please suggest what could be any other process that is blocking the UI thread.

Thanks

Couchbase Lite 2’s query listener implementation runs the queries on the database’s default thread. If that’s the UI thread, it can degrade responsiveness. Workaround is to use a db instance that’s associated with a background thread.

Hi Jens,

Thanks for the help!

We have checked the code and found that queries are somehow running on default thread as you said ( Main Thread).
if we associate the DB instance with a background thread how will it be possible that queries run in that thread instead of the main thread(as default thread is Main Thread)?

Please find below code flow for query change listener -


adding query change listener calls start() method.


Inside this, query chage listener is called without executor as dbListnerToken is null.


If executor is null(changelistener without executor) then it will use Default executor.


and DefaultExecutor uses Main thread looper here. that’s why query change listener always runs on UI thread.

What would you suggest to do? If you can help us use couchbase-lite sdk as an android module then we can fix this and proceed with our further development.

Can we change default executor to single thread executor and use it.

Thanks!

I’m not familiar with the Java implementation. @blake.meike?

Yes. Exactly. Use a different executor. You might consider AsyncTask.THREAD_POOL_EXECUTOR

Hi jens | priya,

@priya.rajagopal as per our disccusion this should be fixed by associate db instance with background thread but it’s not. I have updated couchbase lite 2.5.0. Thanks for early release.

Couchbase Lite 2’s query listener implementation runs the queries on the database’s default thread.
Not agree as per code queries always run on DefaultExecutor. and DefaultExecutor uses main looper(Looper.getMainLooper()) which makes queries run inside main thread(UI thread).
If we want to run query listener implementation in database’s default thread then we have to use current thread looper (Looper.getMyLooper()). please correct me if I am wrong.

Workaround is to use a db instance that’s associated with a background thread.
I have checked by associating db instance to a background thread, but still UI freeze( running inside UI thread).

Suggestion

  1. DefaultExecutor implementation change, we can use Looper.getMyLooper() in place of Looper.getMainLooper() which always run queries in DB associated thread.
  2. Inject executor in Start() method of LiveQuery class. It will fix our problem.
  3. Change DefaultExecutor to a new single thread executor.

Please suggest us better approach from them or other work around.
@priya.rajagopal I apologize for the urgency, but could you provide us solution as soon as possible.

Thanks!

FYI, I’m not involved in the Java implementation; @blake.meike is in charge of that.

Sorry Jens, I appreciate your quick responses and solutions. I agree with your suggestion with this issue but implementation is not as per that according to my analysis. Thank you.

Hey @blake.meike,
Could you please help us in this?

This smells like a bug to me. If there is a custom executor specified on the call to add a change listener to a query then I think it ought to be reused when the internal call to add a database change listener is made (unless this kind of thing will cause a deadlock?)

1 Like

Thanks @borrrden,

Finally, someone understood the issue :wink:
So you mean to say that my 2nd point is right. To inject executor in start method and use that same in database change listener will fix the issue here.

@priya.rajagopal could you please provide this fix in next version of 2.5.x. This is a big issue we are facing (application freeze) and due to this, we are not able to release our product.

Thanks!

I’m not 100% sure that is the correct answer because of my unfamiliarity with Java executors and how they schedule things and if these blocks are async or not. I work in the .NET version and we don’t have any concept of a main thread in a cross platform sense so by default it uses a thread pool background thread for the same operation here. This could be another alternative to having the default executor be the main thread if indeed that is what is happening.

Hey @pankaj.jangid!
I just checked carefully and I think that there may be a misunderstanding about what threads are used where. Live queries are run on an Executor belonging to the database. Each DB has its own Executor, and each executor has one thread. Queries are not run on the DefaultExecutor.
Query results are delivered on the main thread. This is probably what most applications will want. If you need to have the results delivered on another thread, you can register your listener with the LiveQuery.addChangeListener overload that takes an Executor as a parameter. If you do that, the results will be delivered from one of the executor threads.
It is true that the notification of a change in the db is trampolined through the main thread: LiveQuery's changed method is called on the main thread. That method, however does almost nothing: just a little bit of math and then scheduling the query. I would be very surprised if, even when updates were coming in very quickly, it had any noticeable affect on the UI.
There are other possible problems in the LiveQuery code. If you are still experiencing this issue, even after specifying an executor in LiveQuery.addChangeListener, please let me know.
Hope this helps!!

Hi Blake,

Thanks for the explanation, I totally agree with your words.
And we have already tried LiveQuery.addChangeListener method with Executor as a parameter, and this whole post regarding this.

It is true that the notification of a change in the db is trampolined through the main thread: LiveQuery 's changed method is called on the main thread. That method, however does almost nothing: just a little bit of math and then scheduling the query

Yes, this little bit code called on main thread, when few changes come through continuous replication it’s working fine but whenever multiple changes coming through replication our application freeze, I hope you can understand the problem.

Is there some kind of profiling you can do to find out what’s running on the main thread and for how long ? (On iOS I would recommend Instruments, but I don’t know what comparable tool Android has.) We seem to be going around in circles here, and what you’re saying doesn’t match up with our analysis of the code.

The Query running a small piece of code on the main thread should not cause problems with performance of that thread … unless maybe you’re running something on the main thread that blocks until the query updates, which would trigger a deadlock.

At the very least, you could run the app until the main thread visibly locks up, then break into the debugger and get a backtrace, at least of the main thread but preferably all threads.

@pankaj.jangid: I suspect that neither I, nor you, understand your problem very well!

Most of your application runs on the UI thread, but you have selected a tiny little bit of code, that simply schedules a job on an executor, as the cause of the delay. I challenge you to demonstrate that that is actually the problem.

In particular, I very much suspect that the code that actually delivers the results (the QueryChangeListener that is called when the query needs to be updated) is also running on the main thread. It is much more likely that that code is doing heavy lifting and stalling the UI.

Can you show the code that registers a QueryChangeListener with the LiveQuery? … or, perhaps, as @jens suggests, provide more evidence of what code is actually running when the UI stalls?

Hi @blake.meike,

As I have mentioned above that the UI stall when a lot of changes comes. and yesterday the scenario occurs.
I have captured that using Android Profiler, Please finds highlighted(red dotted) time taken by LiveQuery.change() method in below screenshot.



You can see LiveQuery.chagned() method takes 4.68 seconds, 3.96 seconds and 1.44 seconds, which block the UI thread and stalling the UI. If needed I can provide you this recorded session of profiler so you can check.

In particular, I very much suspect that the code that actually delivers the results (the QueryChangeListener that is called when the query needs to be updated) is also running on the main thread. It is much more likely that that code is doing heavy lifting and stalling the UI.
QueryChangeListener.changed() this method calls in the main thread(if used without executor) and we are doing some logic but in a different thread (QueryChangeEventsHandler ) like below.

        @Override
        public void changed(QueryChange change) {
           try {
                QueryChangeEventsHandler.getInstance().put(eventType, change, queryChangeListenerId);
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

If you need anything else please let me know and correct me if my analysis is wrong.

Thanks!

Thanks for the profiling! Most excellent!

I believe that the problem is here:

@Override
public void changed(@NonNull DatabaseChange change) {
    synchronized (lock) {
         // code that takes approximately 0 time to run
    }
}

The issue is that there are long running tasks (e.g., queries) that use the same lock. As a result, the changed method is suspended for long periods of time.

I think that you could confirm this by performing the same analysis, but looking at Thread time, instead of Wall clock time

Regardless, I am working on a fix.

1 Like

Thanks, Blake

The issue is that there are long-running tasks (e.g., queries) that use the same lock. As a result, the changed method is suspended for long periods of time.
Totally agree, you can see in below screenshot of the profiling and according to that LiveQuery.update() method takes time and takes a lock for a long time.
As you suggested I have changed to Thread time which should I do before.
On the same recorded profiler session, I have checked with Thread Time and you can find details below.

According to wall clock timeLiveQuery.change() method takes total 36 seconds in selected time frame. as it takes time due to LiveQuery.update() method which acquire the lock for long time. it’s clear that LiveQuery.change() method taking 650 milliseconds around actually( as per Thread Time which give actual CPU usage time by the thread)

As per above analysis, I confirm that we are on right path.