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

Performance #41

Closed
ecdpalma opened this issue Jul 2, 2016 · 5 comments
Closed

Performance #41

ecdpalma opened this issue Jul 2, 2016 · 5 comments
Assignees
Labels
question / library Issue containing question / discussion about library workings

Comments

@ecdpalma
Copy link

ecdpalma commented Jul 2, 2016

Has anyone noticed performance degradation by using the library? Or is it a common issue? I've been achieving, on average, 1.5kbits/s when exchanging data with another Android device.

@dariuszseweryn dariuszseweryn added the question / library Issue containing question / discussion about library workings label Jul 11, 2016
@dariuszseweryn
Copy link
Owner

No detailed tests were performed.
Usually BLE is not about sending a bulk of data quickly but adding additional layers of abstraction and using RxJava obviously adds some computation work for the processor and memory allocation.
If I would have enough time I would perform some comparison test between specialised implementation of Android BLE API vs RxAndroidBle in firmware update scenario.

@maoueh
Copy link
Contributor

maoueh commented Dec 14, 2016

We are currently using RxAndroidBle to flash the firmware of a device over the air. The firmware is about 300kb. Nice thing is that I also have a proof of concept app for the same flashing process using pure Android API.

These are not real experiments but simple rough estimate. Using RxAndroidBle, it takes up to 3 minutes to transfer everything as for native API, it's about 40s.

The big bottleneck in the library right now for heavy data transfer is that all write operations (in fact simply all operations) are done on the UI thread. This make the transfer process really longer than expected as it's fighting for the UI thread resources.

We noted a minimal 2x increase when just shutting down the screen of the device for example.

I read in the code that you're doing this because Samsung 4.3 fails when connectGatt operation is not performed on the UI thread. Do you think it would be possible to make a conditional to only run the operations on the UI thread for a subset of all devices?

@uKL
Copy link
Collaborator

uKL commented Apr 6, 2017

#165

@uKL
Copy link
Collaborator

uKL commented Apr 6, 2017

As a result of this issue, there will be a report of write operation benchmark (16kB) with nRF based mock device:

  1. Write with native Android APIs (write with BluetoothGatt and direct use of BluetoothGattCallback)
  2. Write with LongWriteOperationBuilder
  3. Looped writeCharacteristic operation of this library
  4. Custom operation with write with BluetoothGatt and response with RxBleGattCallback.
  5. Custom operation with write with BluetoothGatt and direct use of BluetoothGattCallback.

@uKL uKL self-assigned this Apr 6, 2017
dariuszseweryn added a commit that referenced this issue Aug 3, 2017
)

Initially when working with the Android Bluetooth API (18 / Android 4.3) it was found that specific implementations (Samsung’s) cannot work correctly if called directly in callbacks from other calls. To mitigate the problem the next interaction was routed to Android main thread. Now when investigating the issue it turned out that it may be any other thread as long as it is not the callback thread.
@dariuszseweryn
Copy link
Owner

dariuszseweryn commented Oct 2, 2017

Now I finally made some performance tests for sending out as much data as possible using various approaches.

Specification

Peripheral: nRF51822 with SoftDevice S110 8.0.0
Connection Interval: 11.25 ms
MTU: 23 (default—nRF51 does not support different MTUs)
Library version: 1.4.1

Test Algorithm

  1. The central (phone) is sending 19 packets with 20 bytes each to the peripheral. The first byte contains the index of the packet.
  2. After every 19 packets the peripheral sends a notification to the central with indexes of the packets it received. The first byte contains the index of the response. (i.e. the first response packet would contain [0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], second [1, 19, 20, 21, ...])
  3. The central receives the notification and repeats from the first step.

Speed calculation

Every response notification (2. step) is timestamped. All responses are buffered for 60 seconds. We take the last notification timestamp and the first notification timestamp and calculate the time difference. The resulting speed is ((number of received notifications - 1) * number of packets for each notification (19) * number of bytes in each packet (20)) / time difference

Code:

Observable.concat(
        perform(device, this::unoptimizedSendUuid).map(aFloat -> "Unoptimized UUID: " + String.valueOf(aFloat) + " Bps"),
        Observable.<String>empty().delay(1, TimeUnit.SECONDS),
        perform(device, this::unoptimizedSendCharacteristic).map(aFloat -> "Unoptimized Char: " + String.valueOf(aFloat) + " Bps"),
        Observable.<String>empty().delay(1, TimeUnit.SECONDS),
        perform(device, this::longWriteSend).map(aFloat -> "Long Write: " + String.valueOf(aFloat) + " Bps"),
        Observable.<String>empty().delay(1, TimeUnit.SECONDS),
        perform(device, this::optimizedSend).map(aFloat -> "Optimized: " + String.valueOf(aFloat) + " Bps")
)
        .observeOn(AndroidSchedulers.mainThread())
        .doOnUnsubscribe(this::clearSubscription)
        .subscribe(
                result -> Log.e("RESULT", result),
                e -> Log.e("ERROR", "Whoops!", e)
        );

private Observable<Float> perform(RxBleDevice device, TestSetup testSetup) {
    return device.establishConnection(false)
            .flatMap(RxBleConnection::discoverServices,
                    (connection, services) -> services.getCharacteristic(genericCommunicationCharacteristicUuid)
                            .flatMap(
                                    connection::setupNotification,
                                    (characteristic, responseObs) -> {
                                        characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
                                        final Completable readyForTransmission =
                                                responseObs.filter(bytes -> bytes[0] == 0x01).first().toCompletable();
                                        final Observable<byte[]> speedTest = testSetup
                                                .create(new TestConnection(connection, responseObs, characteristic, 19));
                                        return readyForTransmission.andThen(speedTest);
                                    }
                            )
                            .flatMap(observable -> observable)
            )
            .flatMap(observable -> observable)
            .timestamp()
            .buffer(1, TimeUnit.MINUTES)
            .take(1)
            .map(timestampeds -> {
                final int bytesSent = (timestampeds.size() - 1) * 20 * 19;
                final float timeSeconds = (timestampeds.get(timestampeds.size() - 1).getTimestampMillis() - timestampeds.get(0).getTimestampMillis()) * 0.001f;
                return bytesSent / timeSeconds;
            });
}

interface TestSetup {

    Observable<byte[]> create(TestConnection testConnection);
}

static class TestConnection {

    final RxBleConnection connection;

    final Observable<byte[]> responseObs;

    final BluetoothGattCharacteristic characteristic;

    final int batchCount;

    public TestConnection(RxBleConnection connection, Observable<byte[]> responseObs,
                          BluetoothGattCharacteristic characteristic, int batchCount) {
        this.connection = connection;
        this.responseObs = responseObs;
        this.characteristic = characteristic;
        this.batchCount = batchCount;
    }
}

I have compared three approaches to sending data (all of them with setting BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE):

  1. Using `RxBleConnection.writeCharacteristic(UUID, byte[])
private Observable<byte[]> unoptimizedSendUuid(TestConnection testConnection) {
    final RxBleConnection connection = testConnection.connection;
    final BluetoothGattCharacteristic characteristic = testConnection.characteristic;
    final UUID uuid = characteristic.getUuid();
    return Observable.range(0, testConnection.batchCount)
            .concatMap(frameIndex -> connection.writeCharacteristic(
                    uuid,
                    new byte[]{
                            frameIndex.byteValue(), 0, 0, 0, 0, 0, 0, 0, 0,
                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
                    }
            ))
            .ignoreElements()
            .repeatWhen(observable -> Observable.zip(observable, testConnection.responseObs, (o, bytes) -> o))
            .mergeWith(testConnection.responseObs);
}
  1. Using `RxBleConnection.writeCharacteristic(BluetoothGattCharacteristic, byte[])
private Observable<byte[]> unoptimizedSendCharacteristic(TestConnection testConnection) {
    final RxBleConnection connection = testConnection.connection;
    final BluetoothGattCharacteristic characteristic = testConnection.characteristic;
    return Observable.range(0, testConnection.batchCount)
            .concatMap(frameIndex -> connection.writeCharacteristic(
                    characteristic,
                    new byte[]{
                            frameIndex.byteValue(), 0, 0, 0, 0, 0, 0, 0, 0,
                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
                    }
            ))
            .ignoreElements()
            .repeatWhen(observable -> Observable.zip(observable, testConnection.responseObs, (o, bytes) -> o))
            .mergeWith(testConnection.responseObs);
}
  1. Using RxBleConnection.createNewLongWriteBuilder()
private Observable<byte[]> longWriteSend(TestConnection testConnection) {
    final byte[] bytesToSend = new byte[20 * 19];
    for (int i = 0; i < bytesToSend.length; i = i + 20) {
        bytesToSend[i] = (byte) (i / 20);
    }
    return testConnection.connection
            .createNewLongWriteBuilder()
            .setBytes(bytesToSend)
            .setCharacteristic(testConnection.characteristic)
            .build()
            .ignoreElements()
            .repeatWhen(observable -> Observable.zip(observable, testConnection.responseObs, (o, bytes) -> o))
            .mergeWith(testConnection.responseObs);
}
  1. Using RxBleConnection.queue(RxBleCustomOperation)
    Please note that this implementation is not a example to follow—it does not check for the disconnection of the peripheral or cancellation of the operation.
private Observable<byte[]> optimizedSend(TestConnection testConnection) {
    return testConnection.connection.queue((bluetoothGatt, rxBleGattCallback, scheduler) -> Observable.create(
            emitter -> {
                Log.i("START", String.valueOf(testConnection.batchCount));
                final byte[] data = new byte[20];
                testConnection.characteristic.setValue(data);
                final AtomicBoolean writeCompleted = new AtomicBoolean(false);
                final AtomicBoolean ackCompleted = new AtomicBoolean(false);
                final AtomicInteger batchesSent = new AtomicInteger(0);
                final Runnable writeNextBatch = () -> {
                    data[0]++;
                    if (!bluetoothGatt.writeCharacteristic(testConnection.characteristic)) {
                        emitter.onError(new BleGattCannotStartException(bluetoothGatt, BleGattOperationType.CHARACTERISTIC_WRITE));
                    } else {
                        Log.i("SEND", String.valueOf(data[0]));
                        batchesSent.incrementAndGet();
                    }
                };
                final BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
                    @Override
                    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
                        if (status != BluetoothGatt.GATT_SUCCESS) {
                            emitter.onError(new BleGattException(gatt, status, BleGattOperationType.CHARACTERISTIC_WRITE));
                        } else if (batchesSent.get() == testConnection.batchCount) {
                            if (ackCompleted.get()) {
                                batchesSent.set(0);
                                ackCompleted.set(false);
                                emitter.onNext(null);
                                writeNextBatch.run();
                            } else {
                                writeCompleted.set(true);
                            }
                        } else {
                            characteristic.setValue(data);
                            writeNextBatch.run();
                        }
                    }

                    @Override
                    public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
                        final byte[] bytes = characteristic.getValue();
                        Log.i("ACK", Arrays.toString(bytes) + "/" + bytes.length + "/" + System.identityHashCode(bytes));
                        characteristic.setValue(data);
                        if (writeCompleted.get()) {
                            batchesSent.set(0);
                            writeCompleted.set(false);
                            emitter.onNext(null);
                            writeNextBatch.run();
                        } else {
                            ackCompleted.set(true);
                        }
                    }
                };

                rxBleGattCallback.setNativeCallback(bluetoothGattCallback);

                Log.i("SEND", String.valueOf(data[0]));
                if (!bluetoothGatt.writeCharacteristic(testConnection.characteristic)) {
                    emitter.onError(new BleGattCannotStartException(bluetoothGatt, BleGattOperationType.CHARACTERISTIC_WRITE));
                } else {
                    batchesSent.incrementAndGet();
                }
            },
            Emitter.BackpressureMode.NONE
    ));
}

Results (in Bytes per second)

central \ implementation unoptimizedSendUuid unoptimizedSendCharacteristic longWriteSend optimizedSend
Micromax Canvas A107 (5.0) 2463.4 2544.2 3479.5 4604.4
Nexus 5 (6.0.1) 2555.1 2593.2 2657.8 2759.1
Samsung Galaxy S6 SM-G920F (7.0) 1631.9 1789.4 2290.3 3867.7
Motorola Droid XT1030 (4.4.4) 1737.2 1960.8 2707.6 3010.1
Asus Zenfone 5 T00J (4.4.2) 1589.2 1758.0 2450.2 4010.2
Google Pixel (8.0.0) 2513.5 2646.9 2835.7 4602.5

Results for Connection Interval = 100 ms

central \ implementation unoptimizedSendUuid unoptimizedSendCharacteristic longWriteSend optimizedSend
Micromax Canvas A107 (5.0) 576.0 571.9 567.8 570.8
Nexus 5 (6.0.1) 605.0 612.0 623.9 624.5
Samsung Galaxy S6 SM-G920F (7.0) 629.0 619.9 623.2 632.3
Motorola Droid XT1030 (4.4.4) 342.3 342.9 343.0 344.8
Asus Zenfone 5 T00J (4.4.2) 546.7 542.9 560.7 545.7
Samsung Galaxy S3 GT-I9300 (4.3) 579.2 585.6 577.3 568.7
Google Pixel (8.0.0) 757.3 757.3 758.6 758.7

Conclusion

  • When interacting with a low Connection Interval peripheral it may be worth to use native implementation or custom operation to mitigate the downturn that RxJava causes.
  • When the Connection Interval grows the benefit from optimisations are less visible. It could need a detailed research but it seems that for Connection Interval > 50 ms every implementation should perform similarly.
  • The potential throughput of different Android handsets varies a lot (even in the same environment) because of the raw speed of the device (the route time of going through all the system layers from BluetoothGatt.writeCharacteristic() to BluetoothGattCallback.onCharacteristicWrite()) and number of buffers that it's Bluetooth Chip has.

I hope this quick research helps you.

Best Regards

This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question / library Issue containing question / discussion about library workings
Projects
None yet
Development

No branches or pull requests

4 participants