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

Coroutines — Large NOTIFY gets unbearably slow #651

Closed
fonix232 opened this issue Nov 22, 2019 · 31 comments
Closed

Coroutines — Large NOTIFY gets unbearably slow #651

fonix232 opened this issue Nov 22, 2019 · 31 comments
Labels
bug Bug that is caused by the library invalid

Comments

@fonix232
Copy link

fonix232 commented Nov 22, 2019

Original topic:

Large NOTIFY gets unbearably slow

Summary

In my project, I'm connecting to a device that transfers a relatively large amount of data (up to 16MB) via a characteristic's NOTIFY. The equivalent code on iOS works fine, however on Android, using RxAndroidBle, it slows down to an unbearably slow speed the more data it receives.

MTU is set properly, and ends up being 247 (250 - 3b header), which is the maximum supported on the devices. iOS uses a lower MTU, 162 (165 - 3b header).

I've tried requesting lower MTU to see if it helps, to no avail.

The code is pretty simple, after the connection is established (using ReplayingShare), it creates the subscription via setupNotification.

The transfer starts out with pretty decent speed, 1MB takes around 1 minute to sync, however around 3MB received, the speed drops, and the next MB takes 2 minutes, the next MB then 3-4 minutes, and the transfer time keeps creeping up.

Initially I thought it was my fault, as I was storing the data the worst possible way:

var rawData = ByteArray(0)

// in the subscribe block:
rawData += it

However even after removing the storage of the data, the slow-down still happens.

Library version

1.10.4

Preconditions

Steps to reproduce actual result

Not sure if it's reproducible as I have not seen any devices that use a similar technique to transfer data stored on them.

Actual result

The speed of the transfer drops exponentially (or rather, the amount of time required to transfer the same amount of data increases exponentially). The time between the logged Transferred $x out of $y events also increases (initially it's around 10-20ms between messages, which increases to around 100-120ms by the time I transferred ~3MB).

Expected result

Speed should be somewhat constant, with minimum variation between the times required to transfer the same sized blocks.

Minimum code snippet reproducing the issue

var transferred = 0
connection
    ?.flatMap { it.setupNotification(id) }
    ?.flatMap { it }
    ?.subscribe(
        {
            transferred += it.size
            logger.i("[DataSync] Transferred $transferred out of $available")
        },
        { logger.e(it, "[DataSync] Exception happened") },
        { logger.i("[DataSync] Transfer complete!") }
    )
    .addToEventStream(eventStream)
@dariuszseweryn
Copy link
Owner

Hi,
Thanks for the report — have you investigated the issue? Perhaps connection parameters are changing?

@fonix232
Copy link
Author

Yes, I've done some extensive investigation. Initially I thought it was my stupid way of handling the received data (I was recreating the ByteArray by calling rawData += it in subscribe, where rawData is a var ByteArray), however even moving to a preallocated ByteBuffer had the same issue. That's when I switched to the above size checking method, and it was showing the same issues - even though it was just an int field increment.

GC is also going crazy during these events, even without storing the received ByteArray, at the end running around every 200-300ms and freeing up megabytes of data.

I have a feeling there's some background caching going on that causes this issue.

@dariuszseweryn
Copy link
Owner

I have a feeling there's some background caching going on that causes this issue.

There is nothing like this with notifications that I know of. Have you checked the connection parameters? What are they set to? How often and how big notifications are?

@fonix232
Copy link
Author

Have you checked the connection parameters?

Yes I did. PDU is phone-dependent, MTU is always 250.

Connection params on connection:

onConnectionUpdated(), status=0, interval=6 (7.50 ms), latency=0, timeout=500 (5000 ms)

First update (immediately after connection):

onConnectionUpdated(), status=0, interval=36 (45.00 ms), latency=0, timeout=500 (5000 ms)

After a transfer:

onConnectionUpdated(), status=0, interval=24 (30.00 ms), latency=0, timeout=500 (5000 ms)

After each transfer (when the unsubscribe from the NOTIFY characteristic happens), the above connection update happens.

Based on this, the message interval should be 30ms, which roughly correlates to my previous observation of roughly 10-20ms between messages (which technically was rather around 18-28 after doing some proper math, instead of rough estimate). However a longer transfer still increases the interval from this 30ms to around 150-200ms by the time 4MB is transferred, and even higher if a transfer is ongoing. Connection updates are not present during this.

@dariuszseweryn
Copy link
Owner

Have you monitored the app? What is getting garbage collected — what not? Is this download the only thing that is getting the notifications?

@fonix232
Copy link
Author

fonix232 commented Nov 26, 2019

Yes, I'm monitoring the app - and the RAM usage goes up the moment I start the transfer, and keeps going up. GC calls are mostly explicit, but it does not state the location. There are also some background calls, but they're like 1 out of 100-120 explicit calls. And yes, this is the only flow that receives the notifications - a single instance of the RxBleConnection handler, and a single instance of this class.

Did a quick test, just measuring the intervals between received messages, and the intervals clearly increase - we begin at around 10-15ms, and by the time I receive around 10k notifications, it grows to 60ms between messages. Around 20k messages, average interval grows to around 120ms, and it skyrockets higher and higher. Tested the device with other solutions (nRF Connect), and it produced even lower intervals between messages (around 3-6ms!) continuously throughout the transfer, so it's definitely not the device, or the underlying core BLE bits.

@dariuszseweryn
Copy link
Owner

This may be true — easy API usually have performance limitations — basing on RxJava definitely adds some more allocations + additional library abstraction. Your use-case is quite peculiar. You can create an RxBleCustomOperation and operate directly on BluetoothGatt and BluetoothGattCallback to cut out the middle man from equation.

@fonix232
Copy link
Author

That's probably what I'll do, unless I just straight out replace RxAndroidBle with nRF's solution (it fits in better/easier with my current Kotlin Coroutines structure anyway). I also have to write a custom operation to be able to receive the GATT response codes (the device I'm working on has some custom GATT opcodes that I need to process, but not as exceptions - and most of them aren't even thrown as BleGattCharacteristicExceptions), so this might be just the way I go on short term.

@dariuszseweryn
Copy link
Owner

I would love to know what will be your results when getting notifications via custom operation. As for the inability to get GATT codes and BleGattCharacteristicExceptions I am surprised — could you create a separate issue for that so we could dig into it?

I had just looked into the source code but all onCharacteristicWrite() non GATT_SUCCESS GATT codes should be wrapped into BleGattCharacteristicException. All exceptions can be caught in RxJava code and unwrapped as separate results i.e.

connection.writeCharacteristic(uuid, data)
    .map(toYourResultWithData)
    .onErrorResumeNext(throwableToResultWithYourCustomGattCodeOrExceptionOtherwise)

@fonix232
Copy link
Author

fonix232 commented Nov 29, 2019

@dariuszseweryn interesting thing with the RxBleCustomOperation - everything works (characteristic read-write, descriptor read-write), however after enabling notifications on the specific characteristic, I get no calls to onCharacteristicChanged. The following gist is the main part of my code (some identifying bits redacted): https://gist.github.com/fonix232/3d9fb22f25ecf7f3a1094bf6f6088c2b

I'm fairly certain I'm doing the NOTIFY subscription properly, and the device also thinks the notifications went through (NOTIFY verifies every package with the device if it was received, and after some time the device shows that all data was successfully transferred). As you can see I have a log line before any sort of filtering of the incoming change happens, and that line never shows up.

Is it possible that even though I pass the native callback, the onCharacteristicChanged bit doesn't get called?

(Note: I do plan adding some further error handling to this bit, as there are still a handful of failure points)

@dariuszseweryn
Copy link
Owner

That is the exact funny thing about Android's BLE API. It is not easy to get it right.
See BluetoothGatt#setCharacteristicNotification — you must call it to register for notifications internally on the system — this and setting CCC descriptor are needed to get callbacks.

@fonix232
Copy link
Author

Ah, indeed you're right. Multiple sources suggested that writing the descriptor with BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE value will also trigger setCharacteristicNotification() internally. This is not the case. All is working fine now, I will be reporting back sometime next week to see if this actually improves transfer speeds!

@fonix232
Copy link
Author

fonix232 commented Dec 2, 2019

Report time!

The custom implementation, as above, indeed performs as expected - no memory spikes in Profiler, no GC going crazy, and transfer speeds are quite optimal - the previously mentioned 8MB and 16MB transfers complete in about 2 minutes 30 seconds and roughly 5 minutes. Which is much better than the direct RxAndroidBle notify's 16-20 minutes, and is in the expected range of transfer times.

Something is definitely going wrong in the Rx stack here. I understand that this won't affect most users as NOTIFY is mostly used to have periodic updates, however if anyone copies this method of transferring larger data chunks, they will definitely run into the same issue.

@dariuszseweryn
Copy link
Owner

Glad to hear that using a custom operation helped your case.

however if anyone copies this method of transferring larger data chunks, they will definitely run into the same issue

That is the usual problem with performant vs. easy-to-use APIs. If you want to get performance you should strive for bare-metal implementations, which in case of Android BLE APIs are not exactly rookie friendly and can lead to a nonfunctional Android BLE stack and/or phone models incompatibility. This library aims for the ease-of-use and is highly reliant on RxJava which on one hand makes it easy to implement thread safe code and on the other adds (quite a lot of) additional allocations.

RxBleCustomOperation was meant for some advanced use-cases that need access to bare Android API to provide functionality that is not exposed otherwise and/or better performance.

@fonix232
Copy link
Author

fonix232 commented Dec 2, 2019

This library aims for the ease-of-use and is highly reliant on RxJava which on one hand makes it easy to implement thread safe code and on the other adds (quite a lot of) additional allocations.

Yes, but shouldn't a transition from a callback to a simple Observable<> be somewhat linear? The increase in time is far worse than the usual overhead Rx adds, and the incrementing time between packets/messages/onNexts is what worries me. It seriously feels like there's some buffering going on which causes this issue at the core.

@dariuszseweryn
Copy link
Owner

It should be linear. I am pretty sure there is no buffering. There is at least one explicit object allocation and a thread switch (which may be the cause of increasing times if it is overwhelmed with events). Other allocations are related to RxJava internals. Maybe it would be possible to optimise current code. I do not know what device/os you have tested on.

@fonix232
Copy link
Author

fonix232 commented Dec 2, 2019

That's what I'd expect too, based on my prior experience with RxJava. I mean, if Rx can generally provide a streamlined access to massive, fast-streaming objects (a previous project of mine, where news articles were broken up into blocks and stored in an SQL database, and Rx was used to stream these objects into the actual article display objects), the same should apply to fast-streaming, but small (250b) objects as well. The thread switching part does sound suspicious though.

Testing was done on various devices - Pixel 1 and 2, a bunch of Samsung phones (S6, S7, S8+, Note8, Note9), OnePlus devices (really poor Bluetooth experience in general, but that's a device fault), and some Sony phones. All of them presented the same issue, the transfer time increase between 10% blocks was almost exponential (our best result was a 10-25-60-120-250-280s increase, and that was when I stopped the test at 80% - though I believe the increase started "evening out" at the end, given the small jump from 250s to 280s), and got unbearably slow around the end.

@dariuszseweryn
Copy link
Owner

You may try commenting out the thread switching code (.observeOn() or .delay() depending on the code version) in RxBleGattCallback#getOnCharacteristicChanged and check if it helps in your case. I wonder what is the exact problem here.

@fonix232
Copy link
Author

fonix232 commented Dec 2, 2019

I will see if I have any time - unfortunately I've got a bunch of deadlines before the holidays, and even this three-day party with custom operations have delayed some important work. But it's definitely worth taking a look at, I think - though I understand why you can't test it without any devices that emulate this behaviour.

@dariuszseweryn
Copy link
Owner

I have patched up an nRF52 (PCA10056) based peripheral that spams notifications with MTU 512

Test code

Test code based on 1.11.0-SNAPSHOT (sha1 3c02d19173588ac66da593201fa1d143663ba7b1):

rxBleClient.scanBleDevices(
    ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build(),
    ScanFilter.Builder().setServiceUuid(ParcelUuid(serviceUuid)).build()
)
    .take(1)
    .flatMap { it.bleDevice.establishConnection(false) }
    .flatMap { c ->
        c.setupNotification(settingsUuid)
            .flatMap { dataObs ->
                val throughputEvery10Seconds = dataObs
                    .window(10, TimeUnit.SECONDS)
                    .flatMapSingle { it.reduce(0, { sum, data -> sum + data.size }) }
                    .map { it.toFloat() / 10.0f }
                c.requestMtu(512).ignoreElement()
                    .andThen(throughputEvery10Seconds)
            }
    }
    .subscribe(
        { Log.e("SPEED", "$it Bps") },
        { Log.e("SPEED", "Whoops!", it) }
    )

For reference: Your target average throughput is 8MB / 2.5 min = ~56000 Bps.

Test results:

Samsung Galaxy S8 (SM-G950F) / OS: API 26 (Android 8.0)

Profiling did not show energy usage above 10%

2019-12-06 16:26:49.395 22024-22138/com.polidea.rxandroidble.sample D/BluetoothLeScanner: Start Scan with callback
2019-12-06 16:26:49.407 22024-22125/com.polidea.rxandroidble.sample D/BluetoothLeScanner: onScannerRegistered() - status=0 scannerId=8 mScannerId=0
2019-12-06 16:26:49.594 22024-22138/com.polidea.rxandroidble.sample D/BluetoothAdapter: STATE_ON
2019-12-06 16:26:49.597 22024-22138/com.polidea.rxandroidble.sample I/chatty: uid=10511(u0_a511) pool-1-thread-1 identical 2 lines
2019-12-06 16:26:49.599 22024-22138/com.polidea.rxandroidble.sample D/BluetoothAdapter: STATE_ON
2019-12-06 16:26:49.599 22024-22138/com.polidea.rxandroidble.sample D/BluetoothLeScanner: Stop Scan with callback
2019-12-06 16:26:49.630 22024-22138/com.polidea.rxandroidble.sample D/BluetoothGatt: connect() - device: D3:4D:73:E8:41:1F, auto: false
2019-12-06 16:26:49.631 22024-22138/com.polidea.rxandroidble.sample D/BluetoothAdapter: isSecureModeEnabled
2019-12-06 16:26:49.636 22024-22138/com.polidea.rxandroidble.sample D/BluetoothGatt: registerApp()
2019-12-06 16:26:49.637 22024-22138/com.polidea.rxandroidble.sample D/BluetoothGatt: registerApp() - UUID=ec08f470-489a-4ade-b2b4-c362f789aff5
2019-12-06 16:26:49.645 22024-22125/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientRegistered() - status=0 clientIf=8
2019-12-06 16:26:50.093 22024-22125/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=8 device=D3:4D:73:E8:41:1F
2019-12-06 16:26:50.163 22024-22138/com.polidea.rxandroidble.sample D/BluetoothGatt: discoverServices() - device: D3:4D:73:E8:41:1F
2019-12-06 16:26:50.507 22024-22125/com.polidea.rxandroidble.sample D/BluetoothGatt: onConnectionUpdated() - Device=D3:4D:73:E8:41:1F interval=6 latency=0 timeout=500 status=0
2019-12-06 16:26:50.664 22024-22125/com.polidea.rxandroidble.sample D/BluetoothGatt: onSearchComplete() = Device=D3:4D:73:E8:41:1F Status=0
2019-12-06 16:26:50.725 22024-22125/com.polidea.rxandroidble.sample D/BluetoothGatt: onConnectionUpdated() - Device=D3:4D:73:E8:41:1F interval=39 latency=0 timeout=500 status=0
2019-12-06 16:26:50.776 22024-22140/com.polidea.rxandroidble.sample D/BluetoothGatt: setCharacteristicNotification() - uuid: 432101ff-5678-9012-3456-789012345678 enable: true
2019-12-06 16:26:50.786 22024-22138/com.polidea.rxandroidble.sample D/BluetoothGatt: configureMTU() - device: D3:4D:73:E8:41:1F mtu: 512
2019-12-06 16:26:50.871 22024-22125/com.polidea.rxandroidble.sample D/BluetoothGatt: onConfigureMTU() - Device=D3:4D:73:E8:41:1F mtu=512 status=0
2019-12-06 16:26:52.385 22024-22029/com.polidea.rxandroidble.sample I/zygote64: Do partial code cache collection, code=24KB, data=28KB
2019-12-06 16:26:52.386 22024-22029/com.polidea.rxandroidble.sample I/zygote64: After code cache collection, code=24KB, data=28KB
2019-12-06 16:26:52.386 22024-22029/com.polidea.rxandroidble.sample I/zygote64: Increasing code cache capacity to 128KB
2019-12-06 16:26:55.277 22024-22037/com.polidea.rxandroidble.sample D/BluetoothGatt: onConnectionUpdated() - Device=D3:4D:73:E8:41:1F interval=30 latency=0 timeout=200 status=0
2019-12-06 16:26:58.781 22024-22029/com.polidea.rxandroidble.sample I/zygote64: Do partial code cache collection, code=61KB, data=51KB
2019-12-06 16:26:58.782 22024-22029/com.polidea.rxandroidble.sample I/zygote64: After code cache collection, code=61KB, data=51KB
2019-12-06 16:26:58.782 22024-22029/com.polidea.rxandroidble.sample I/zygote64: Increasing code cache capacity to 256KB
2019-12-06 16:27:00.882 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 70191.1 Bps
2019-12-06 16:27:10.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 75026.6 Bps
2019-12-06 16:27:20.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77011.7 Bps
2019-12-06 16:27:30.882 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77724.3 Bps
2019-12-06 16:27:40.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78080.6 Bps
2019-12-06 16:27:50.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 75433.8 Bps
2019-12-06 16:28:00.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78386.0 Bps
2019-12-06 16:28:10.881 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76044.6 Bps
2019-12-06 16:28:20.881 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 74466.7 Bps
2019-12-06 16:28:30.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76859.0 Bps
2019-12-06 16:28:40.885 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 74619.4 Bps
2019-12-06 16:28:50.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76706.3 Bps
2019-12-06 16:29:00.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76197.3 Bps
2019-12-06 16:29:10.885 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 74619.4 Bps
2019-12-06 16:29:20.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76553.6 Bps
2019-12-06 16:29:30.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 75535.6 Bps
2019-12-06 16:29:40.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76350.0 Bps
2019-12-06 16:29:50.882 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77215.3 Bps
2019-12-06 16:30:00.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 75230.2 Bps
2019-12-06 16:30:10.885 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78080.6 Bps
2019-12-06 16:30:20.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78640.5 Bps
2019-12-06 16:30:30.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78436.9 Bps
2019-12-06 16:30:40.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 79505.8 Bps
2019-12-06 16:30:50.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77368.0 Bps
2019-12-06 16:31:00.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76655.4 Bps
2019-12-06 16:31:10.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77927.9 Bps
2019-12-06 16:31:20.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76299.1 Bps
2019-12-06 16:31:30.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78029.7 Bps
2019-12-06 16:31:40.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 76960.8 Bps
2019-12-06 16:31:50.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77062.6 Bps
2019-12-06 16:32:00.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77164.4 Bps
2019-12-06 16:32:10.884 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 77978.8 Bps
2019-12-06 16:32:20.885 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78029.7 Bps
2019-12-06 16:32:30.885 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78640.5 Bps
2019-12-06 16:32:40.885 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 79047.7 Bps
2019-12-06 16:32:50.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78742.3 Bps
2019-12-06 16:33:00.883 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 79200.4 Bps
2019-12-06 16:33:10.882 22024-22156/com.polidea.rxandroidble.sample E/SPEED: 78793.2 Bps

OnePlus 3T (A3003) / API 26 (Android 8.0.0)

2019-12-06 16:36:48.085 11150-13371/com.polidea.rxandroidble.sample D/BluetoothAdapter: isLeEnabled(): ON
2019-12-06 16:36:48.093 11150-11196/com.polidea.rxandroidble.sample D/BluetoothLeScanner: onScannerRegistered() - status=0 scannerId=7 mScannerId=0
2019-12-06 16:36:48.862 11150-13371/com.polidea.rxandroidble.sample D/BluetoothAdapter: isLeEnabled(): ON
2019-12-06 16:36:48.881 11150-13371/com.polidea.rxandroidble.sample D/BluetoothGatt: connect() - device: D3:4D:73:E8:41:1F, auto: false
2019-12-06 16:36:48.881 11150-13371/com.polidea.rxandroidble.sample D/BluetoothGatt: registerApp()
2019-12-06 16:36:48.882 11150-13371/com.polidea.rxandroidble.sample D/BluetoothGatt: registerApp() - UUID=f181da00-3c77-4b5f-8b04-f9581c0337e0
2019-12-06 16:36:48.887 11150-11196/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientRegistered() - status=0 clientIf=7
2019-12-06 16:36:49.474 11150-11196/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=7 device=D3:4D:73:E8:41:1F
2019-12-06 16:36:49.490 11150-13371/com.polidea.rxandroidble.sample D/BluetoothGatt: discoverServices() - device: D3:4D:73:E8:41:1F
2019-12-06 16:36:49.872 11150-11196/com.polidea.rxandroidble.sample D/BluetoothGatt: onConnectionUpdated() - Device=D3:4D:73:E8:41:1F interval=6 latency=0 timeout=500 status=0
2019-12-06 16:36:50.078 11150-11196/com.polidea.rxandroidble.sample D/BluetoothGatt: onSearchComplete() = Device=D3:4D:73:E8:41:1F Status=0
2019-12-06 16:36:50.112 11150-13413/com.polidea.rxandroidble.sample D/BluetoothGatt: setCharacteristicNotification() - uuid: 432101ff-5678-9012-3456-789012345678 enable: true
2019-12-06 16:36:50.120 11150-13371/com.polidea.rxandroidble.sample D/BluetoothGatt: configureMTU() - device: D3:4D:73:E8:41:1F mtu: 512
2019-12-06 16:36:50.133 11150-11196/com.polidea.rxandroidble.sample D/BluetoothGatt: onConfigureMTU() - Device=D3:4D:73:E8:41:1F mtu=512 status=0
2019-12-06 16:36:50.140 11150-11196/com.polidea.rxandroidble.sample D/BluetoothGatt: onConnectionUpdated() - Device=D3:4D:73:E8:41:1F interval=36 latency=0 timeout=500 status=0
2019-12-06 16:36:54.613 11150-11181/com.polidea.rxandroidble.sample D/BluetoothGatt: onConnectionUpdated() - Device=D3:4D:73:E8:41:1F interval=30 latency=0 timeout=200 status=0
2019-12-06 16:36:59.715 11150-11171/com.polidea.rxandroidble.sample I/zygote64: Do full code cache collection, code=124KB, data=104KB
2019-12-06 16:36:59.716 11150-11171/com.polidea.rxandroidble.sample I/zygote64: After code cache collection, code=104KB, data=61KB
2019-12-06 16:37:00.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 53954.0 Bps
2019-12-06 16:37:10.142 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 58789.5 Bps
2019-12-06 16:37:20.146 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 61741.7 Bps
2019-12-06 16:37:30.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 60978.2 Bps
2019-12-06 16:37:40.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 58789.5 Bps
2019-12-06 16:37:50.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 57975.1 Bps
2019-12-06 16:38:00.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 60265.6 Bps
2019-12-06 16:38:10.142 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67391.6 Bps
2019-12-06 16:38:18.547 11150-11171/com.polidea.rxandroidble.sample I/zygote64: Do partial code cache collection, code=123KB, data=73KB
2019-12-06 16:38:18.549 11150-11171/com.polidea.rxandroidble.sample I/zygote64: After code cache collection, code=123KB, data=73KB
2019-12-06 16:38:18.549 11150-11171/com.polidea.rxandroidble.sample I/zygote64: Increasing code cache capacity to 512KB
2019-12-06 16:38:20.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67442.5 Bps
2019-12-06 16:38:30.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67747.9 Bps
2019-12-06 16:38:40.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67544.3 Bps
2019-12-06 16:38:50.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 64235.8 Bps
2019-12-06 16:39:00.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65508.3 Bps
2019-12-06 16:39:10.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65457.4 Bps
2019-12-06 16:39:20.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67086.2 Bps
2019-12-06 16:39:30.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 66322.7 Bps
2019-12-06 16:39:40.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67849.7 Bps
2019-12-06 16:39:50.142 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 68460.5 Bps
2019-12-06 16:40:00.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 63981.3 Bps
2019-12-06 16:40:10.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65152.0 Bps
2019-12-06 16:40:20.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 68409.6 Bps
2019-12-06 16:40:30.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 66984.4 Bps
2019-12-06 16:40:40.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65813.7 Bps
2019-12-06 16:40:50.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65711.9 Bps
2019-12-06 16:41:00.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 66424.5 Bps
2019-12-06 16:41:10.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65813.7 Bps
2019-12-06 16:41:20.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67646.1 Bps
2019-12-06 16:41:30.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 66322.7 Bps
2019-12-06 16:41:40.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65966.4 Bps
2019-12-06 16:41:50.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67137.1 Bps
2019-12-06 16:42:00.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 66475.4 Bps
2019-12-06 16:42:10.144 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 64948.4 Bps
2019-12-06 16:42:20.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67137.1 Bps
2019-12-06 16:42:30.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 65813.7 Bps
2019-12-06 16:42:40.143 11150-13414/com.polidea.rxandroidble.sample E/SPEED: 67238.9 Bps

Samsung Galaxy S6 (SM-G920F) / API 24 (Android 7.0)

It is very slow compared to S8 or 3T but it also seem to provide stable performance.

2019-12-06 17:54:29.396 18920-19481/com.polidea.rxandroidble.sample D/BluetoothGatt: connect() - device: D3:4D:73:E8:41:XX, auto: false
2019-12-06 17:54:29.396 18920-19481/com.polidea.rxandroidble.sample D/BluetoothAdapter: isSecureModeEnabled
2019-12-06 17:54:29.397 18920-19481/com.polidea.rxandroidble.sample D/BluetoothGatt: registerApp()
2019-12-06 17:54:29.398 18920-19481/com.polidea.rxandroidble.sample D/BluetoothGatt: registerApp() - UUID=201d8f04-2c5b-4559-af94-404214fb311f
2019-12-06 17:54:29.502 18920-18933/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientRegistered() - status=0 clientIf=7
2019-12-06 17:54:30.181 18920-18942/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientConnectionState() - status=0 clientIf=7 device=D3:4D:73:E8:41:XX
2019-12-06 17:54:30.240 18920-19481/com.polidea.rxandroidble.sample D/BluetoothGatt: discoverServices() - device: D3:4D:73:E8:41:XX
2019-12-06 17:54:30.439 18920-18932/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientConnParamsChanged() - Device=D3:4D:73:E8:41:XX interval=6 status=0
2019-12-06 17:54:30.596 18920-18933/com.polidea.rxandroidble.sample D/BluetoothGatt: onSearchComplete() = Device=D3:4D:73:E8:41:XX Status=0
2019-12-06 17:54:30.632 18920-19484/com.polidea.rxandroidble.sample D/BluetoothGatt: setCharacteristicNotification() - uuid: 432101ff-5678-9012-3456-789012345678 enable: true
2019-12-06 17:54:30.641 18920-18932/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientConnParamsChanged() - Device=D3:4D:73:E8:41:XX interval=39 status=0
2019-12-06 17:54:30.644 18920-19481/com.polidea.rxandroidble.sample D/BluetoothGatt: configureMTU() - device: D3:4D:73:E8:41:XX mtu: 512
2019-12-06 17:54:32.016 18920-18942/com.polidea.rxandroidble.sample D/BluetoothGatt: onConfigureMTU() - Device=D3:4D:73:E8:41:XX mtu=512 status=0
2019-12-06 17:54:36.740 18920-18933/com.polidea.rxandroidble.sample D/BluetoothGatt: onClientConnParamsChanged() - Device=D3:4D:73:E8:41:XX interval=30 status=0
2019-12-06 17:54:42.039 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2799.5 Bps
2019-12-06 17:54:52.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 17:55:02.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 17:55:12.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 17:55:22.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 17:55:32.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 17:55:42.036 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 17:55:52.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 17:56:02.036 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 17:56:12.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3257.6 Bps
2019-12-06 17:56:22.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3206.7 Bps
2019-12-06 17:56:32.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 17:56:42.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3206.7 Bps
2019-12-06 17:56:52.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2901.3 Bps
2019-12-06 17:57:02.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2748.6 Bps
2019-12-06 17:57:12.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2748.6 Bps
2019-12-06 17:57:22.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2901.3 Bps
2019-12-06 17:57:32.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2901.3 Bps
2019-12-06 17:57:42.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2850.4 Bps
2019-12-06 17:57:52.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2901.3 Bps
2019-12-06 17:58:02.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2901.3 Bps
2019-12-06 17:58:12.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 17:58:22.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 17:58:32.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2850.4 Bps
2019-12-06 17:58:42.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2952.2 Bps
2019-12-06 17:58:52.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 17:59:02.036 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 17:59:12.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 17:59:22.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 17:59:32.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 17:59:42.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2952.2 Bps
2019-12-06 17:59:52.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 18:00:02.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 18:00:12.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2748.6 Bps
2019-12-06 18:00:22.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2901.3 Bps
2019-12-06 18:00:32.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 18:00:42.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 18:00:52.039 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 18:01:02.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 18:01:12.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 18:01:22.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 18:01:32.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 18:01:42.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3206.7 Bps
2019-12-06 18:01:52.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 18:02:02.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 18:02:12.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 18:02:22.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3155.8 Bps
2019-12-06 18:02:32.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 18:02:42.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 18:02:52.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3104.9 Bps
2019-12-06 18:03:02.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3003.1 Bps
2019-12-06 18:03:12.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2901.3 Bps
2019-12-06 18:03:22.038 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 2952.2 Bps
2019-12-06 18:03:32.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps
2019-12-06 18:03:42.037 18920-19545/com.polidea.rxandroidble.sample E/SPEED: 3054.0 Bps

Bottom line

I cannot recreate issues you are facing. I have two ideas on what may potentially be wrong:

  1. My test code is not reflecting your situation well enough — I have focused on measuring throughput using sheer RxJava operators
  2. connection observable in your test code is a shared one and a different place in the app does something that is crippling the performance

Question

Do you see what may be different between our setups?

@fonix232
Copy link
Author

fonix232 commented Dec 6, 2019

Well, for starters, I'm still using 1.10.4, though I doubt it means much of a difference.

Then, the actual difference - I'm using an abstraction layer that takes an RxBleDevice, and wraps its functions with Kotlin Coroutines. A quick excerpt of what I'm doing: https://gist.github.com/fonix232/fef86e6124431d012b2c42734589041b

As you can see, I've managed to cobble together a quite performant little Coroutine wrapper (the rest of the app also uses Coroutines, but there's simply no usable solution for BLE on Android that works nice with them, and changing the rest of the app to work Rx-style would be a challenge at this point).

However I don't see how Coroutines could cause this issue - there's no thread switching, no heavy computation, and the coroutines-rx conversion is also pretty lightweight on the Channel subscription (see e.g. enableNotifyAsync).

@dariuszseweryn
Copy link
Owner

@fonix232
Copy link
Author

fonix232 commented Dec 7, 2019

@dariuszseweryn
Copy link
Owner

I might have find the culprit of high memory consumption in your code. .openSubscription() creates a SubscriptionChannel that is a LinkedListChannel — which as you can read has an unlimited buffer.
It seems that if the producer is quicker than consumer the buffer will get filled — slowing down the machine due to amount of work needed to allocate a bigger buffer.

@fonix232
Copy link
Author

fonix232 commented Dec 8, 2019

Hmm, that's definitely interesting, and would explain why the slowdown happens only on larger transfers (e.g. an 8MB transfer shows now slowing down up until ~90% at worst, usually none at all).

@dariuszseweryn what do you recon, would a batching operation on the Rx end of things possibly mitigate the problem? Given the packet size and frequency, I'm thinking about either using your window() approach with an update, say, every second, or possibly by size (say, every kB) - but all of this would depend on the transfer size and MTU of course.

@dariuszseweryn
Copy link
Owner

what do you recon, would a batching operation on the Rx end of things possibly mitigate the problem?

If the issue (and so it seems) is poor performance of data consumer then batching the data should decrease amount of "thread" switches — this may be a good path to take. Batching by time should be better approach I suppose.

I have also checked the performance of version 1.10.5 on Samsung Galaxy S8 / API 26 but results were comparable. 1.10.5 has a fix for a situation where notifications from two (or more) different characteristics could be emitted in different order than they arrived — @fonix232 in your case this should not be a problem since you mentioned you use single characteristic notifications.
Test vs notifications with MTU 23 (~25kB / ~1300 events per second) gave around 10% CPU utilisation. No memory spikes were observed.

This topic is somewhat related to #41

@fonix232
Copy link
Author

fonix232 commented Dec 9, 2019

What I'm testing now is moving the custom operations' emitter.onNext() into a clause that only gets run if the percentage changes. In this case, this would mean that instead of some ~70k onNext calls, there would be a total of 101 messages (100x onNext + 1 onComplete). However sadly there doesn't seem to be any improvements - in case of a 16MB transfer, the speeds still drop around the end (and sometimes even in the middle - it's quite random and honestly I can't even wrap my head around where the bottleneck might be, as even RxBleLog reports the received packages arriving at larger and larger intervals). On the other hand, said message intervals do equalise out at the end - after a few messages (roughly 1-2% worth), the intervals become pretty stable, although high (initially I'd receive a message, logged, every 10-15ms, which then jumps up to 300-400ms per batches).

@dariuszseweryn
Copy link
Owner

I may not be the best person to give advices about kotlin coroutines as I have little experience with them — I just try to rule out that the library is the culprit of bad performance.

The thread you use for processing BLE data may be shared with different parts of the code which may indirectly degrade performance of the subject code. Or the processing itself (disc IO?) is slow? Hard to say for me as I am not a part of your project and can only make a stub on my end — sample code seems to work with acceptable performance.

@fonix232
Copy link
Author

fonix232 commented Dec 9, 2019

Except I even tested with no processing - not even storage of the data, just increment a counter - and the slowdown still happens.

I will try adapting your flow into a sample app to see if this issue still happens using pure Rx code and nothing else around it.

@dariuszseweryn
Copy link
Owner

We have different results in our tests. You could prepare a repository which I could just copy and install that shows the problem so I can play with it. Without it cannot do much more.

@dariuszseweryn
Copy link
Owner

Two weeks have passed. I am closing this issue. Still — feel free to share a minimal project that displays the issue — I could then start digging /experimenting from there and reopen the topic. Although it seems not related directly to the library, having experience with coroutines could help in the future or give hints whether a coroutines API is feasible for BLE.

@dariuszseweryn dariuszseweryn changed the title Large NOTIFY gets unbearably slow Coroutines — Large NOTIFY gets unbearably slow Dec 23, 2019
@dariuszseweryn dariuszseweryn added the bug Bug that is caused by the library label Jan 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Bug that is caused by the library invalid
Projects
None yet
Development

No branches or pull requests

2 participants