From 476e6f3e9290b7fb6cc319c234962934583654fd Mon Sep 17 00:00:00 2001 From: "hiddenalpha.ch" <23085769+hiddenalpha@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:47:30 +0200 Subject: [PATCH] Reduce busy-waiting CPU load in waitEvents() 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. Edit: > - try fix error: > "Compatibility with CMake < 3.5 has been removed from CMake." > - Use an older ubuntu to broaden runtime compatiblity > - try fix error: > "Failed to locate 'make', requesting installation of command line developer tools" > - Revert some non-working MacOS fixes > - Explain why to use older ubuntu version > - Update checked-in JNI header file as generated by JDK > - GuessFix MacOS CICD > https://github.com/java-native/jssc/pull/201#issuecomment-4724269095 > - Drop unneccessary compatibility symbols. as discussed in: > https://github.com/java-native/jssc/pull/201#discussion_r3421753438 > - Cleanup JavaDoc comment > - Whops. Fix the log-damp counter. > - Drop convenience overload > See https://github.com/java-native/jssc/pull/201#discussion_r3462608811 > - Allow `-1` in setter to disable again. > See https://github.com/java-native/jssc/pull/201#discussion_r3462537778 > - Give hint about using 100ms > https://github.com/java-native/jssc/pull/201#issuecomment-4782713639 > - Skip timeout in EventThread ctor while state init > https://github.com/java-native/jssc/pull/201#discussion_r3462949002 Squashed-From: 03704d8858e140f62de3032d8ac07a76674f7021 --- .github/workflows/build.yml | 4 +- CMakeLists.txt | 2 +- src/main/cpp/_nix_based/jssc.cpp | 41 +++++++++- src/main/cpp/jssc_SerialNativeInterface.h | 6 +- src/main/cpp/windows/jssc.cpp | 3 +- src/main/java/jssc/SerialNativeInterface.java | 4 +- src/main/java/jssc/SerialPort.java | 42 +++++++++- src/test/java/jssc/SerialPortTest.java | 78 +++++++++++++++++++ 8 files changed, 169 insertions(+), 11 deletions(-) create mode 100644 src/test/java/jssc/SerialPortTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e715240d8..f18c4d0ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,7 +52,7 @@ jobs: - run: mvn -P "${{ matrix.profile }}" --batch-mode macos: - runs-on: [macos-latest] + runs-on: [macos-14] strategy: fail-fast: false matrix: @@ -106,4 +106,4 @@ jobs: java-version: 11 distribution: temurin - - run: mvn -P "${{ matrix.profile }}" --batch-mode \ No newline at end of file + - run: mvn -P "${{ matrix.profile }}" --batch-mode diff --git a/CMakeLists.txt b/CMakeLists.txt index a3b53cf49..8bc7d82b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.0) +cmake_minimum_required(VERSION 3.5) cmake_policy(SET CMP0048 NEW) cmake_policy(SET CMP0042 NEW) diff --git a/src/main/cpp/_nix_based/jssc.cpp b/src/main/cpp/_nix_based/jssc.cpp index cab25db09..45a3b75f2 100644 --- a/src/main/cpp/_nix_based/jssc.cpp +++ b/src/main/cpp/_nix_based/jssc.cpp @@ -907,13 +907,52 @@ const jint events[] = {INTERRUPT_BREAK, //EV_RXFLAG, //Not supported EV_TXEMPTY}; + /* OK */ /* * Collecting data for EventListener class (Linux have no implementation of "WaitCommEvent" function from Windows) * */ JNIEXPORT jobjectArray JNICALL Java_jssc_SerialNativeInterface_waitEvents - (JNIEnv *env, jobject, jlong portHandle) { + ( JNIEnv*env, jobject, jlong portHandle, jint waitEventsTimeoutMs) { + int err; + + /* 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 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. But we do not have a + * win32-API here. As this impl here returns immediately, we'll + * first ask `poll()` (if available and enabled). This way we can + * "emulate" to actually wait if nothing is ready. + * See also JavaDoc of `SerialPort.setWaitEventsTimeoutMs(int)`. */ + int const isFeatureEnabled = (waitEventsTimeoutMs >= 1); + if( isFeatureEnabled ){ +#if !HAVE_POLL + static unsigned cnt = 0; + if( ((cnt++) & 0xFFFF) == 0 ){ + fprintf(stderr, "WARN: waitEventsTimeoutMs not available on your platform, as `poll()` not available.\n"); + } +#else + struct pollfd pfd = {0}; + pfd.fd = portHandle; + pfd.events = POLLIN | POLLPRI | POLLRDHUP; + err = poll(&pfd, 1, waitEventsTimeoutMs); + if( err == -1 ) switch( errno ){ + case EINTR: + /* Got interrupted by signal. Go report events we have so far. */ + break; + default: + /* some error occurred. */ + err = errno; /* bkup `errno` before calling into `FindClass()` */ + jclass exClz = env->FindClass("java/lang/RuntimeException"); + if( exClz ) env->ThrowNew(exClz, strerror(err)); + return NULL; + } +#endif + } jclass intClass = env->FindClass("[I"); if( intClass == NULL ) return NULL; diff --git a/src/main/cpp/jssc_SerialNativeInterface.h b/src/main/cpp/jssc_SerialNativeInterface.h index afde9b032..c202f0311 100644 --- a/src/main/cpp/jssc_SerialNativeInterface.h +++ b/src/main/cpp/jssc_SerialNativeInterface.h @@ -82,10 +82,10 @@ JNIEXPORT jint JNICALL Java_jssc_SerialNativeInterface_getEventsMask /* * Class: jssc_SerialNativeInterface * Method: waitEvents - * Signature: (J)[[I + * Signature: (JI)[[I */ JNIEXPORT jobjectArray JNICALL Java_jssc_SerialNativeInterface_waitEvents - (JNIEnv *, jobject, jlong); + (JNIEnv *, jobject, jlong, jint); /* * Class: jssc_SerialNativeInterface @@ -170,4 +170,4 @@ JNIEXPORT jboolean JNICALL Java_jssc_SerialNativeInterface_sendBreak #ifdef __cplusplus } #endif -#endif \ No newline at end of file +#endif diff --git a/src/main/cpp/windows/jssc.cpp b/src/main/cpp/windows/jssc.cpp index 0f08718f0..e4ede43c1 100644 --- a/src/main/cpp/windows/jssc.cpp +++ b/src/main/cpp/windows/jssc.cpp @@ -447,12 +447,13 @@ JNIEXPORT jboolean JNICALL Java_jssc_SerialNativeInterface_sendBreak return returnValue; } + /* * Wait event * portHandle - port handle */ JNIEXPORT jobjectArray JNICALL Java_jssc_SerialNativeInterface_waitEvents - (JNIEnv *env, jobject, jlong portHandle) { + ( JNIEnv*env, jobject, jlong portHandle, jint/*unused on windows*/ ){ HANDLE hComm = (HANDLE)portHandle; DWORD lpEvtMask = 0; DWORD lpNumberOfBytesTransferred = 0; diff --git a/src/main/java/jssc/SerialNativeInterface.java b/src/main/java/jssc/SerialNativeInterface.java index 091dc3848..84cc8a099 100644 --- a/src/main/java/jssc/SerialNativeInterface.java +++ b/src/main/java/jssc/SerialNativeInterface.java @@ -234,10 +234,12 @@ public static String getLibraryVersion() { * * @param handle handle of opened port * + * @param waitEventsTimeoutMs See {@link SerialPort#setWaitEventsTimeoutMs(int)}. + * * @return Method returns two-dimensional array containing event types and their values * (events[i][0] - event type, events[i][1] - event value). */ - public native int[][] waitEvents(long handle); + public native int[][] waitEvents(long handle, int waitEventsTimeoutMs); /** * Change RTS line state diff --git a/src/main/java/jssc/SerialPort.java b/src/main/java/jssc/SerialPort.java index a203bdec2..e4bafce9b 100644 --- a/src/main/java/jssc/SerialPort.java +++ b/src/main/java/jssc/SerialPort.java @@ -41,6 +41,7 @@ public class SerialPort { private final String portName; private volatile boolean portOpened = false; private boolean maskAssigned = false; + private volatile int waitEventsTimeoutMs = -1; //since 2.2.0 -> private volatile Method methodErrorOccurred = null; @@ -920,6 +921,43 @@ public boolean setFlowControlMode(int mask) throws SerialPortException { return serialInterface.setFlowControlMode(portHandle, mask); } + /** + * Reduce busy-waiting CPU load in {@link #waitEvents()}. + * + * (This is irrelevant for windows) + * + * 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. + * + * This setting can be used to reduce that load. For regular + * incoming data events this does not cause any further delays. + * {@link #waitEvents()} still will reports most of the events as + * soon they become available, even before the specified timeout got + * reached. + * + * Choosing "good value" solely depends on the callers use-case. I + * know of a project which works perfectly fine using 100ms. + * + * Special values: Pass `-1` to explicitly disable the feature + * (You'll likely not need this, as feature is disabled by default + * anyway). Passing any other negative values is NOT allowed. + * Passing `0` is NOT allowed. Instead, disable the feature if you + * need "no timeout". + * + * BUT BE AWARE: Enabling this might delay delivery of some special + * serial-events (like 'DCD line changed' or 'RI line changed') by + * the amount of time specified. So you have to decide yourself if + * you can/will afford this trade. + */ + public void setWaitEventsTimeoutMs(int waitEventsTimeoutMs) { + if (waitEventsTimeoutMs <= 0 && waitEventsTimeoutMs != -1) { + throw new IllegalArgumentException(String.valueOf(waitEventsTimeoutMs)); + } + this.waitEventsTimeoutMs = waitEventsTimeoutMs; + } + /** * Get flow control mode * @@ -951,7 +989,7 @@ public boolean sendBreak(int duration)throws SerialPortException { } private int[][] waitEvents() { - return serialInterface.waitEvents(portHandle); + return serialInterface.waitEvents(portHandle, waitEventsTimeoutMs); } /** @@ -1263,7 +1301,7 @@ private class LinuxEventThread extends EventThread { //Need to get initial states public LinuxEventThread(){ - int[][] eventArray = waitEvents(); + int[][] eventArray = serialInterface.waitEvents(portHandle, -1); for(int[] event : eventArray){ int eventType = event[0]; int eventValue = event[1]; diff --git a/src/test/java/jssc/SerialPortTest.java b/src/test/java/jssc/SerialPortTest.java new file mode 100644 index 000000000..8230eb953 --- /dev/null +++ b/src/test/java/jssc/SerialPortTest.java @@ -0,0 +1,78 @@ +package jssc; + +import jssc.junit.rules.DisplayMethodNameRule; +import org.junit.Test; +import org.slf4j.Logger; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.slf4j.LoggerFactory.getLogger; + + +public class SerialPortTest extends DisplayMethodNameRule { + + private static final Logger log = getLogger(SerialPortTest.class); + + + @Test + public void expectSettingToBeSetSuccessfully() { + SerialPort serial = new SerialPort("ttyS0"); + /* 100ms proved to be a reasonable value in the use-case our using + * project had. */ + serial.setWaitEventsTimeoutMs(100); + } + + + /** + * Cannot really test this deeply. Just make sure the setter accepts the + * value. + */ + @Test + public void disableFeatureByPassingMinusOne() { + SerialPort serial = new SerialPort("ttyS0"); + serial.setWaitEventsTimeoutMs(-1); + } + + + /** + * configuring a zero-length timeout doesn't make any sense. I'd expect + * this to have the same effect as using no timeout in the 1st place. + * With the difference, we will have nonsense calls to `poll`. + * + * As soon someone really has the need to pass zero, then inverse this + * test and EXPLAIN CLEARLY by replacing this comment why this is the case. + */ + @Test + public void mustNotPassZero() { + SerialPort serial = new SerialPort("ttyS0"); + try { + serial.setWaitEventsTimeoutMs(0); + fail("Where's the exception?"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("0")); + } + } + + + /** + * Prefer to tell the user right away whenever nonsense values are passed. + * Makes bugs to appear early in place of them hiding silent. + */ + @Test + public void mustNotPassAnyOtherNegativeValues() { + /* just try a bunch of illegal values (testing ALL possible cases might + * take a bit too long) */ + SerialPort serial = new SerialPort("ttyS0"); + for(int badTimeoutMs = -42 ; badTimeoutMs <= -2 ; ++badTimeoutMs ){ + log.debug("setWaitEventsTimeoutMs({})", badTimeoutMs); + try { + serial.setWaitEventsTimeoutMs(badTimeoutMs); + fail("Where's the exception for "+ badTimeoutMs +"?"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains(String.valueOf(badTimeoutMs))); + } + } + } + + +}