flutter_libserialport
flutter_libserialport copied to clipboard
Windows Release High CPU usage
To re-produce the issue,
-
Please compile windows exe using the code at the bottom, which is slightly modified from the original flutter_libserialport example.
-
Run compilation output: example.exe.
-
Click the comport being used for the test,
-
Send some data to the comport.
-
After receiving the data, the CPU usage goes high.
Thank you very much!
Flutter Doctor output:
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel master, 2.3.0-17.0.pre.414, on Microsoft Windows [Version 10.0.18363.719], locale zh-CN)
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
X cmdline-tools component is missing
Run path/to/sdkmanager --install "cmdline-tools;latest"
See https://developer.android.com/studio/command-line for more details.
X Android license status unknown.
Run flutter doctor --android-licenses
to accept the SDK licenses.
See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[√] Chrome - develop for the web
[√] Visual Studio - develop for Windows (Visual Studio Community 2019 16.8.6)
[√] Android Studio (version 4.2.0)
[√] Connected device (3 available)
! Doctor found issues in 1 category.
import 'package:flutter/material.dart';
import 'package:flutter_libserialport/flutter_libserialport.dart';
void main() => runApp(ExampleApp());
class ExampleApp extends StatefulWidget {
@override
_ExampleAppState createState() => _ExampleAppState();
}
extension IntToString on int {
String toHex() => '0x${toRadixString(16)}';
String toPadded([int width = 3]) => toString().padLeft(width, '0');
String toTransport() {
switch (this) {
case SerialPortTransport.usb:
return 'USB';
case SerialPortTransport.bluetooth:
return 'Bluetooth';
case SerialPortTransport.native:
return 'Native';
default:
return 'Unknown';
}
}
}
class _ExampleAppState extends State<ExampleApp> {
var availablePorts = [];
var _portInUse = false;
late SerialPort _port;
late SerialPortReader _reader;
@override
void dispose() {
_reader.close();
_port.close();
super.dispose();
}
@override
void initState() {
super.initState();
initPorts();
}
void initPorts() {
setState(() => availablePorts = SerialPort.availablePorts);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Serial Port example'),
),
body: Scrollbar(
child: ListView(
children: [
for (final address in availablePorts)
Builder(builder: (context) {
final port = SerialPort(address);
return ExpansionTile(
title: TextButton(
child: Text(address),
onPressed: _portInUse
? null
: () {
_port = SerialPort(address);
if (!_port.openReadWrite()) {
print(SerialPort.lastError);
}
_portInUse = true;
SerialPortConfig config = SerialPortConfig();
config.baudRate = 9600;
config.stopBits = 1;
config.bits = 8;
config.setFlowControl(SerialPortFlowControl.none);
_port.config = config;
_reader = SerialPortReader(_port);
_reader.stream.listen((data) {
port.write(data);
});
setState(() {});
},
),
children: [
CardListTile('Description', port.description),
CardListTile('Transport', port.transport.toTransport()),
CardListTile('USB Bus', port.busNumber?.toPadded()),
CardListTile('USB Device', port.deviceNumber?.toPadded()),
CardListTile('Vendor ID', port.vendorId?.toHex()),
CardListTile('Product ID', port.productId?.toHex()),
CardListTile('Manufacturer', port.manufacturer),
CardListTile('Product Name', port.productName),
CardListTile('Serial Number', port.serialNumber),
CardListTile('MAC Address', port.macAddress),
],
);
}),
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.refresh),
onPressed: initPorts,
),
),
);
}
}
class CardListTile extends StatelessWidget {
final String name;
final String? value;
CardListTile(this.name, this.value);
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(value ?? 'N/A'),
subtitle: Text(name),
),
);
}
}
I have the same high CPU usage issue, narrowed it down to listening on SerialPortReader.stream
final reader = SerialPortReader(_sp);
_sub = reader.stream.listen((data) {
/* final List<int> values = [];
for (final v in data) {
values.add(v);
}
_sc.add(values); */
});
if I don't listen on reader
, the cpu usage goes back to normal, if I listen to it, even with an empty listener as shown, the CPU usage goes bad again
Temporary fix:
If I use a timer
instead of SerialPortReader
the problem can be avoided
_t = Timer.periodic(Duration(milliseconds: 40), (timer) {
final data = _sp.read(400);
final List<int> values = [];
for (final v in data) {
values.add(v);
}
if (values.isNotEmpty) _sc.add(values);
});
I'm experiencing the same issue here, the SerialPortReader uses 100% of 1 cpu when you listen to its stream. Timer.periodic is many orders of magnitudes more efficient, even with 1 millisecond interval.
I also experienced this issue and looked into it a little. The reader code uses the sp_wait call to block waiting for rx bytes to be available. This 'should' block the isolate until bytes are received. However, I found this call was always returning immediately even if no bytes have been received. This results in the isolate running in a loop hence the 100% cpu usage of a single core.
I did not have time to investigate the reason for the non-blocking call to sp_wait but I found that it was relatively simple to write an alternative version of SerialPortReader that replaces the sp_wait() call with a blocking call to read() to wait for a single byte to be received. Obviously, you must send that received byte to the send port as it is valid data but then the rest of the code is largely the same.
This will give you the correct low CPU behaviour and low latency.
Here's my alternative version of _waitRead. The rest of the code is pretty much the same with some simplifications to remove the event code which is no longer used. I am not sure how this will behave if there are serial port errors so use with caution. However, in my limited testing it has performed fine.
static void _waitRead(_SerialPortReaderArgs args) {
SerialPort port = SerialPort.fromAddress(args.address);
while (port.isOpen) {
// Read a single byte. This is a blocking call which will timeout after args.timeout ms.
var first = port.read(1, timeout: args.timeout);
if (first.isNotEmpty) {
// Send the single byte
args.sendPort.send(first);
// Read the rest of the data available.
var toRead = port.bytesAvailable;
if (toRead > 0) {
var remaining = port.read(toRead);
if (remaining.isNotEmpty) {
args.sendPort.send(remaining);
}
}
}
}
}
Same issue here, worsened by the fact that my program could have more than one serial port to read from, each greatly increasing the CPU load. I solved by using a timer as suggested above (the 20ms latency is not a problem in my case), but this needs some fixing. Like an option to use either the event system or the first byte workaround.
@stevecookdev Thanks a lot for your workaround. I am using it and it works perfectly.
@stevecookdev Thank for your example code. You mentioned: "I am not sure how this will behave if there are serial port errors so use with caution". And you were right: this code snippet doesn't handle error situations like disconnecting an UART-device, etc. I added the necessary processing:
static void _waitRead(_SerialPortReaderArgs args) {
final fport = ffi.Pointer<sp_port>.fromAddress(args.address);
final events = _createEvents(fport, _kReadEvents);
SerialPort port = SerialPort.fromAddress(args.address);
while (port.isOpen) {
// Read a single byte. This is a blocking call which will timeout after args.timeout ms.
Uint8List first;
try {
first = port.read(1, timeout: args.timeout);
} catch (e) {
args.sendPort.send(SerialPort.lastError);
break;
}
if (first.isNotEmpty) {
// Send the single byte
args.sendPort.send(first);
// Read the rest of the data available.
var toRead = port.bytesAvailable;
if (toRead > 0) {
var remaining = port.read(toRead);
if (remaining.isNotEmpty) {
args.sendPort.send(remaining);
}
}
}
}
_releaseEvents(events);
}