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

Start and stop BLE scans from a background thread to prevent blocking the UI (issue #136) #430

Merged

Conversation

marcosalis
Copy link
Contributor

This pull request fixes the problems reported in issue #136 where ANRs are caused by AltBeacon accessing the BluetoothAdapter (which has heavy internal synchronization and even explicit wait() calls) from the main thread.
A HandlerThread is used for the purpose, and no internal state is accessed from within the posted runnables so that it's not necessary to make the CycledLeScanner classes thread safe. In the same way, callbacks from the BluetoothAdapter are already always executed from the main thread (they're called, internally, from a Handler created with the main looper).

@davidgyoung
Copy link
Member

Thanks, @marcosalis.

One concern I have is that this change appears to enforce that callbacks always happen on the main thread. This may cause ANR problems for existing applications that do not expect this and upgrade to a version of the library with this change. This would happen for any app doing heavy processing in a beacon callback.

Thoughts?

@marcosalis
Copy link
Contributor Author

Thank you for your answer @davidgyoung.
Are you referring to the BluetoothAdapter callbacks into the AltBeacon library, or the "client code" listeners for ranging/monitoring operations?

BluetoothAdapter already delivers all the callback results on the main thread by using a Handler internally, so nothing would change on the interaction with the library (and you don't have to make the CycledLeScanner classes thread safe as their state is only accessed from the main thread).

If you're talking about the former, from what I see in the codebase the callbacks are always executed from BeaconIntentProcessor.onHandleIntent(), which is always executed from a worker thread (see IntentService documentation), so nothing should change in the way AltBeacon delivers its results.
I do think, by the way, that the library should deliver the callbacks on the main thread (as I've commented in PR #149), but as you say it's a breaking change so you might want to consider that for future releases ;)

@marcosalis
Copy link
Contributor Author

@davidgyoung any updates on this? Just as additional information and motivation to implement this change, here's a screenshot of a method profiling on the main thread when the CycledLeScanner is in action in our application.
As you can notice, the BluetoothLeScanner code from the Android SDK is blocking the thread on an Object.wait() for around 20 ms, which is not much in absolute terms but it's already above the "16ms" empirical limit in which the main thread should draw a frame on screen.
screen shot 2016-10-13 at 17 52 58

@davidgyoung
Copy link
Member

Sorry for the delay on this, @marcosalis. I will do some testing on this today.

protected final Handler mHandler = new Handler();

protected final Handler mHandler = new Handler(Looper.getMainLooper());
protected final Handler mScanHandler;
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason to use a HandlerThread here? Why not just construct mScanHandler = new Handler();? Then you don't need to clean up the handler thread later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@davidgyoung see my answer below (and this doc), this is the change that makes the whole PR work: by creating it with new Handler() you'd be using the Looper from the thread that's instantiating CycledLeScanner, which is the main thread.

Instead, by creating a HandlerThread (which is just a thread with a Looper attached to it) we make sure the "expensive" Bluetooth code from the SDK is posted and executed in our own worker thread.

Copy link
Member

Choose a reason for hiding this comment

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

OK, got it now... apologies, it's been awhile, it's been awhile since I looked at this code. I was incorrectly thinking that new Handler() always creates a new thread.

@@ -40,7 +42,10 @@
private long mScanPeriod;

protected long mBetweenScanPeriod;
protected final Handler mHandler = new Handler();

protected final Handler mHandler = new Handler(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.

What is the point of calling Looper.getMainLooper() here? This handler will never need to communicate with the main UI thread, so why not let it have its own looper?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The default, parameterless Handler constructor uses the Looper from the current thread (as documented here and here).

In this particular case, CycledLeScanner is always instantiated from the main thread (from BeaconService.onCreate()) and consequently, that Handler will already use the main thread's looper.
This addition doesn't modify the current logic, it's only a futureproof guarantee that the class behaviour won't change if in the future if CycledLeScanner is instantiated from another thread that doesn't have a Looper (you would immediately notice because the Handler creation would crash).

@davidgyoung
Copy link
Member

I've tested this code, and it works for me, but I have some questions about the handler setup I'd like to resolve before merging.

@davidgyoung
Copy link
Member

Thanks for your patience here, @marcosalis. Another question: Wouldn't it be simpler to create a single Handler on a new thread, and run all of the code in CycledLeScanner on this thread? That way we wouldn't need both mHandler and mScanHandler and the work could be run on the one handler from theCycledLeScanner#start andCycledLeScanner#stop methods.

@marcosalis
Copy link
Contributor Author

@davidgyoung you could use this approach, but then you'd have to make all the CycledLeScanner (and concrete implementations) internal state thread-safe.
At the moment, the internal state is only accessed by one thread (the main thread), so you don't have to complicate your class state and add explicit synchronisation. The mScanHandler is now only used to start/stop the BluetoothAdapter scans and doesn't access any instance field from its class.

@davidgyoung
Copy link
Member

Fair point. Ok, looks good to me.

@davidgyoung davidgyoung merged commit 158ac20 into AltBeacon:master Oct 20, 2016
@marcosalis
Copy link
Contributor Author

Thank you @davidgyoung !

cupakromer added a commit that referenced this pull request May 24, 2017
This makes the current implicit assumption that the scan cycler is
started/stopped on the main thread explicit. This helps quickly inform
developers what the callback expectations are in regards to race
conditions and state safety.

In order to verify this we need to trace the `start`/`stop`/`destroy`
calls back to the service. The service's core methods `onCreate` and
`onDestroy` are called on the main thread (see links below). This means
the service creates it's `mMessenger` on the main thread, which creates
the `IncomingHandler` on the main thread which binds to the main looper.
However, the `IncomingHandler` class itself leaves the looper
unspecified relying on looper for the thread which creates it.

As we did in #430, this modifies `IncomingHandler` to explicitly use the
main looper as a future proofing mechanism. This way we can ensure this
implicit expectation doesn't change or at least make it obvious when
troubleshooting cases where someone expects it to have changed.
Similarly, this adds the main thread annotation to it's `handleMessage`
implementation.

Working from here we can confirm that the only places where the beacon
monitoring/ranging is updated is from the main thread through the
`IncomingHandler`. As the `IncomingHandler` is the main communication
channel for the service we transfer the main thread expectation to the
associated ranging/monitoring methods. _If_ someone decides to call
these methods directly on the service these annotations help the IDEs
check/document that such calls are expected from the main thread.

With all of that documented we can now confidently state that the scan
cycler's `start`, `stop`, and `destroy` are also meant to be called from
the main thread. As `start` and `stop` both call `scanLeDevice` we can
start tracing any other threading expectations for it. We already know
it's called from the main thread through deferred jobs. This leaves the
`finishScanCycle` as the last call site.

`finishScanCycle` is only called from `scheduleScanCycleStop`. As this
method name implies it's called through a deferred job on the main
thread. It is also called the "first" time in `scanLeDevice`. Thus we've
shown that `scanLeDevice`, `finishScanCycle`, and
`scheduleScanCycleStop` are expected to run on the main thread.

See Also:

- https://developer.android.com/reference/android/os/Handler.html#Handler%28%29
- https://developer.android.com/training/multiple-threads/communicate-ui.html
- https://developer.android.com/reference/android/app/Service.html
- https://developer.android.com/guide/components/services.html
- #430
cupakromer added a commit that referenced this pull request May 24, 2017
This makes the current implicit assumption that the scan cycler is
started/stopped on the main thread explicit. This helps quickly inform
developers what the callback expectations are in regards to race
conditions and state safety.

In order to verify this we need to trace the `start`/`stop`/`destroy`
calls back to the service. The service's core methods `onCreate` and
`onDestroy` are called on the main thread (see links below). This means
the service creates it's `mMessenger` on the main thread, which creates
the `IncomingHandler` on the main thread which binds to the main looper.
However, the `IncomingHandler` class itself leaves the looper
unspecified relying on looper for the thread which creates it.

As we did in #430, this modifies `IncomingHandler` to explicitly use the
main looper as a future proofing mechanism. This way we can ensure this
implicit expectation doesn't change or at least make it obvious when
troubleshooting cases where someone expects it to have changed.
Similarly, this adds the main thread annotation to it's `handleMessage`
implementation.

Working from here we can confirm that the only places where the beacon
monitoring/ranging is updated is from the main thread through the
`IncomingHandler`. As the `IncomingHandler` is the main communication
channel for the service we transfer the main thread expectation to the
associated ranging/monitoring methods. _If_ someone decides to call
these methods directly on the service these annotations help the IDEs
check/document that such calls are expected from the main thread.

With all of that documented we can now confidently state that the scan
cycler's `start`, `stop`, and `destroy` are also meant to be called from
the main thread. As `start` and `stop` both call `scanLeDevice` we can
start tracing any other threading expectations for it. We already know
it's called from the main thread through deferred jobs. This leaves the
`finishScanCycle` as the last call site.

`finishScanCycle` is only called from `scheduleScanCycleStop`. As this
method name implies it's called through a deferred job on the main
thread. It is also called the "first" time in `scanLeDevice`. Thus we've
shown that `scanLeDevice`, `finishScanCycle`, and
`scheduleScanCycleStop` are expected to run on the main thread.

See Also:

- https://developer.android.com/reference/android/os/Handler.html#Handler%28%29
- https://developer.android.com/training/multiple-threads/communicate-ui.html
- https://developer.android.com/reference/android/app/Service.html
- https://developer.android.com/guide/components/services.html
- #430
cupakromer added a commit that referenced this pull request May 26, 2017
This makes the current implicit assumption that the scan cycler is
started/stopped on the main thread explicit. This helps quickly inform
developers what the callback expectations are in regards to race
conditions and state safety.

In order to verify this we need to trace the `start`/`stop`/`destroy`
calls back to the service. The service's core methods `onCreate` and
`onDestroy` are called on the main thread (see links below). This means
the service creates it's `mMessenger` on the main thread, which creates
the `IncomingHandler` on the main thread which binds to the main looper.
However, the `IncomingHandler` class itself leaves the looper
unspecified relying on looper for the thread which creates it.

As we did in #430, this modifies `IncomingHandler` to explicitly use the
main looper as a future proofing mechanism. This way we can ensure this
implicit expectation doesn't change or at least make it obvious when
troubleshooting cases where someone expects it to have changed.
Similarly, this adds the main thread annotation to it's `handleMessage`
implementation.

Working from here we can confirm that the only places where the beacon
monitoring/ranging is updated is from the main thread through the
`IncomingHandler`. As the `IncomingHandler` is the main communication
channel for the service we transfer the main thread expectation to the
associated ranging/monitoring methods. _If_ someone decides to call
these methods directly on the service these annotations help the IDEs
check/document that such calls are expected from the main thread.

With all of that documented we can now confidently state that the scan
cycler's `start`, `stop`, and `destroy` are also meant to be called from
the main thread. As `start` and `stop` both call `scanLeDevice` we can
start tracing any other threading expectations for it. We already know
it's called from the main thread through deferred jobs. This leaves the
`finishScanCycle` as the last call site.

`finishScanCycle` is only called from `scheduleScanCycleStop`. As this
method name implies it's called through a deferred job on the main
thread. It is also called the "first" time in `scanLeDevice`. Thus we've
shown that `scanLeDevice`, `finishScanCycle`, and
`scheduleScanCycleStop` are expected to run on the main thread.

See Also:

- https://developer.android.com/reference/android/os/Handler.html#Handler%28%29
- https://developer.android.com/training/multiple-threads/communicate-ui.html
- https://developer.android.com/reference/android/app/Service.html
- https://developer.android.com/guide/components/services.html
- #430
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.

2 participants