Skip to content

Commit 2032c0d

Browse files
authored
Reduce busy-waiting CPU load in waitEvents() (#201)
The {@link #waitEvents()} implementation on non-windows systems usually returns immediately. This is unfortunate for the callers which want to await events in an infinite-loop. As doing so would burn lot of CPU time. For example: Code in `LinuxEventThread.run()` (in `SerialPort.java`) calls us in an infinite-loop. As a work-around, it uses a (very) small sleep, to not utilize a full CPU core all the time. But still, this permanently wastes a lot of CPU cycles (that many, that it is a problem in our production use-case). The win32 code uses `OVERLAPPED` structs and `WaitSingleObject()` which already provide that kind of "wait" mechanism. Not perfect, but this patch at least provides a way to wait if nothing is ready. That "feature" is off by default and can be enabled individually.
1 parent 4b167f2 commit 2032c0d

8 files changed

Lines changed: 169 additions & 11 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ jobs:
5252
- run: mvn -P "${{ matrix.profile }}" --batch-mode
5353

5454
macos:
55-
runs-on: [macos-latest]
55+
runs-on: [macos-14]
5656
strategy:
5757
fail-fast: false
5858
matrix:
@@ -106,4 +106,4 @@ jobs:
106106
java-version: 11
107107
distribution: temurin
108108

109-
- run: mvn -P "${{ matrix.profile }}" --batch-mode
109+
- run: mvn -P "${{ matrix.profile }}" --batch-mode

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
cmake_minimum_required(VERSION 3.0)
1+
cmake_minimum_required(VERSION 3.5)
22
cmake_policy(SET CMP0048 NEW)
33
cmake_policy(SET CMP0042 NEW)
44

src/main/cpp/_nix_based/jssc.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,13 +907,52 @@ const jint events[] = {INTERRUPT_BREAK,
907907
//EV_RXFLAG, //Not supported
908908
EV_TXEMPTY};
909909

910+
910911
/* OK */
911912
/*
912913
* Collecting data for EventListener class (Linux have no implementation of "WaitCommEvent" function from Windows)
913914
*
914915
*/
915916
JNIEXPORT jobjectArray JNICALL Java_jssc_SerialNativeInterface_waitEvents
916-
(JNIEnv *env, jobject, jlong portHandle) {
917+
( JNIEnv*env, jobject, jlong portHandle, jint waitEventsTimeoutMs) {
918+
int err;
919+
920+
/* Code in `LinuxEventThread.run()` (in `SerialPort.java`) calls us
921+
* in an infinite-loop. As a work-around, it uses a (very) small
922+
* sleep, to not utilize a full CPU all the time. But still, this
923+
* permanently wastes a lot of CPU cycles (that many, that it is a
924+
* problem in our production use-case). The win32 code uses
925+
* `OVERLAPPED` structs and `WaitSingleObject()` which already
926+
* provide that kind of "wait" mechanism. But we do not have a
927+
* win32-API here. As this impl here returns immediately, we'll
928+
* first ask `poll()` (if available and enabled). This way we can
929+
* "emulate" to actually wait if nothing is ready.
930+
* See also JavaDoc of `SerialPort.setWaitEventsTimeoutMs(int)`. */
931+
int const isFeatureEnabled = (waitEventsTimeoutMs >= 1);
932+
if( isFeatureEnabled ){
933+
#if !HAVE_POLL
934+
static unsigned cnt = 0;
935+
if( ((cnt++) & 0xFFFF) == 0 ){
936+
fprintf(stderr, "WARN: waitEventsTimeoutMs not available on your platform, as `poll()` not available.\n");
937+
}
938+
#else
939+
struct pollfd pfd = {0};
940+
pfd.fd = portHandle;
941+
pfd.events = POLLIN | POLLPRI | POLLRDHUP;
942+
err = poll(&pfd, 1, waitEventsTimeoutMs);
943+
if( err == -1 ) switch( errno ){
944+
case EINTR:
945+
/* Got interrupted by signal. Go report events we have so far. */
946+
break;
947+
default:
948+
/* some error occurred. */
949+
err = errno; /* bkup `errno` before calling into `FindClass()` */
950+
jclass exClz = env->FindClass("java/lang/RuntimeException");
951+
if( exClz ) env->ThrowNew(exClz, strerror(err));
952+
return NULL;
953+
}
954+
#endif
955+
}
917956

918957
jclass intClass = env->FindClass("[I");
919958
if( intClass == NULL ) return NULL;

src/main/cpp/jssc_SerialNativeInterface.h

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/cpp/windows/jssc.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,12 +447,13 @@ JNIEXPORT jboolean JNICALL Java_jssc_SerialNativeInterface_sendBreak
447447
return returnValue;
448448
}
449449

450+
450451
/*
451452
* Wait event
452453
* portHandle - port handle
453454
*/
454455
JNIEXPORT jobjectArray JNICALL Java_jssc_SerialNativeInterface_waitEvents
455-
(JNIEnv *env, jobject, jlong portHandle) {
456+
( JNIEnv*env, jobject, jlong portHandle, jint/*unused on windows*/ ){
456457
HANDLE hComm = (HANDLE)portHandle;
457458
DWORD lpEvtMask = 0;
458459
DWORD lpNumberOfBytesTransferred = 0;

src/main/java/jssc/SerialNativeInterface.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,12 @@ public static String getLibraryVersion() {
234234
*
235235
* @param handle handle of opened port
236236
*
237+
* @param waitEventsTimeoutMs See {@link SerialPort#setWaitEventsTimeoutMs(int)}.
238+
*
237239
* @return Method returns two-dimensional array containing event types and their values
238240
* (<b>events[i][0] - event type</b>, <b>events[i][1] - event value</b>).
239241
*/
240-
public native int[][] waitEvents(long handle);
242+
public native int[][] waitEvents(long handle, int waitEventsTimeoutMs);
241243

242244
/**
243245
* Change RTS line state

src/main/java/jssc/SerialPort.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public class SerialPort {
4141
private final String portName;
4242
private volatile boolean portOpened = false;
4343
private boolean maskAssigned = false;
44+
private volatile int waitEventsTimeoutMs = -1;
4445

4546
//since 2.2.0 ->
4647
private volatile Method methodErrorOccurred = null;
@@ -920,6 +921,43 @@ public boolean setFlowControlMode(int mask) throws SerialPortException {
920921
return serialInterface.setFlowControlMode(portHandle, mask);
921922
}
922923

924+
/**
925+
* Reduce busy-waiting CPU load in {@link #waitEvents()}.
926+
*
927+
* (This is irrelevant for windows)
928+
*
929+
* The {@link #waitEvents()} implementation on non-windows systems
930+
* usually returns immediately. This is unfortunate for the callers
931+
* which want to await events in an infinite-loop. As doing so would
932+
* burn lot of CPU time.
933+
*
934+
* This setting can be used to reduce that load. For regular
935+
* incoming data events this does not cause any further delays.
936+
* {@link #waitEvents()} still will reports most of the events as
937+
* soon they become available, even before the specified timeout got
938+
* reached.
939+
*
940+
* Choosing "good value" solely depends on the callers use-case. I
941+
* know of a project which works perfectly fine using 100ms.
942+
*
943+
* Special values: Pass `-1` to explicitly disable the feature
944+
* (You'll likely not need this, as feature is disabled by default
945+
* anyway). Passing any other negative values is NOT allowed.
946+
* Passing `0` is NOT allowed. Instead, disable the feature if you
947+
* need "no timeout".
948+
*
949+
* BUT BE AWARE: Enabling this might delay delivery of some special
950+
* serial-events (like 'DCD line changed' or 'RI line changed') by
951+
* the amount of time specified. So you have to decide yourself if
952+
* you can/will afford this trade.
953+
*/
954+
public void setWaitEventsTimeoutMs(int waitEventsTimeoutMs) {
955+
if (waitEventsTimeoutMs <= 0 && waitEventsTimeoutMs != -1) {
956+
throw new IllegalArgumentException(String.valueOf(waitEventsTimeoutMs));
957+
}
958+
this.waitEventsTimeoutMs = waitEventsTimeoutMs;
959+
}
960+
923961
/**
924962
* Get flow control mode
925963
*
@@ -951,7 +989,7 @@ public boolean sendBreak(int duration)throws SerialPortException {
951989
}
952990

953991
private int[][] waitEvents() {
954-
return serialInterface.waitEvents(portHandle);
992+
return serialInterface.waitEvents(portHandle, waitEventsTimeoutMs);
955993
}
956994

957995
/**
@@ -1263,7 +1301,7 @@ private class LinuxEventThread extends EventThread {
12631301

12641302
//Need to get initial states
12651303
public LinuxEventThread(){
1266-
int[][] eventArray = waitEvents();
1304+
int[][] eventArray = serialInterface.waitEvents(portHandle, -1);
12671305
for(int[] event : eventArray){
12681306
int eventType = event[0];
12691307
int eventValue = event[1];
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package jssc;
2+
3+
import jssc.junit.rules.DisplayMethodNameRule;
4+
import org.junit.Test;
5+
import org.slf4j.Logger;
6+
7+
import static org.junit.Assert.assertTrue;
8+
import static org.junit.Assert.fail;
9+
import static org.slf4j.LoggerFactory.getLogger;
10+
11+
12+
public class SerialPortTest extends DisplayMethodNameRule {
13+
14+
private static final Logger log = getLogger(SerialPortTest.class);
15+
16+
17+
@Test
18+
public void expectSettingToBeSetSuccessfully() {
19+
SerialPort serial = new SerialPort("ttyS0");
20+
/* 100ms proved to be a reasonable value in the use-case our using
21+
* project had. */
22+
serial.setWaitEventsTimeoutMs(100);
23+
}
24+
25+
26+
/**
27+
* Cannot really test this deeply. Just make sure the setter accepts the
28+
* value.
29+
*/
30+
@Test
31+
public void disableFeatureByPassingMinusOne() {
32+
SerialPort serial = new SerialPort("ttyS0");
33+
serial.setWaitEventsTimeoutMs(-1);
34+
}
35+
36+
37+
/**
38+
* configuring a zero-length timeout doesn't make any sense. I'd expect
39+
* this to have the same effect as using no timeout in the 1st place.
40+
* With the difference, we will have nonsense calls to `poll`.
41+
*
42+
* As soon someone really has the need to pass zero, then inverse this
43+
* test and EXPLAIN CLEARLY by replacing this comment why this is the case.
44+
*/
45+
@Test
46+
public void mustNotPassZero() {
47+
SerialPort serial = new SerialPort("ttyS0");
48+
try {
49+
serial.setWaitEventsTimeoutMs(0);
50+
fail("Where's the exception?");
51+
} catch (IllegalArgumentException e) {
52+
assertTrue(e.getMessage().contains("0"));
53+
}
54+
}
55+
56+
57+
/**
58+
* Prefer to tell the user right away whenever nonsense values are passed.
59+
* Makes bugs to appear early in place of them hiding silent.
60+
*/
61+
@Test
62+
public void mustNotPassAnyOtherNegativeValues() {
63+
/* just try a bunch of illegal values (testing ALL possible cases might
64+
* take a bit too long) */
65+
SerialPort serial = new SerialPort("ttyS0");
66+
for(int badTimeoutMs = -42 ; badTimeoutMs <= -2 ; ++badTimeoutMs ){
67+
log.debug("setWaitEventsTimeoutMs({})", badTimeoutMs);
68+
try {
69+
serial.setWaitEventsTimeoutMs(badTimeoutMs);
70+
fail("Where's the exception for "+ badTimeoutMs +"?");
71+
} catch (IllegalArgumentException e) {
72+
assertTrue(e.getMessage().contains(String.valueOf(badTimeoutMs)));
73+
}
74+
}
75+
}
76+
77+
78+
}

0 commit comments

Comments
 (0)