From 51f8683c9343fea3f8b2d4be531aa61f6efc0f55 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sun, 26 Oct 2025 04:15:23 +0000 Subject: [PATCH 01/20] Align masks with Posix policy --- README.adoc | 1 + .../net/openhft/affinity/impl/CpuSetUtil.java | 38 ++++++++++++++ .../affinity/impl/PosixJNAAffinity.java | 15 +++--- .../openhft/affinity/impl/CpuSetUtilTest.java | 34 ++++++++++++ .../affinity/impl/PosixAffinityMaskTest.java | 52 +++++++++++++++++++ 5 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/CpuSetUtilTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/PosixAffinityMaskTest.java diff --git a/README.adoc b/README.adoc index 61367bd58..ff68e799a 100644 --- a/README.adoc +++ b/README.adoc @@ -79,6 +79,7 @@ For example: * `-Daffinity.reserved=2` reserves only CPU `1`. * `-Daffinity.reserved=6` reserves CPUs `1` and `2`. * `-Daffinity.reserved=10` reserves CPUs `1` and `3` (hexadecimal `a`). +* `-Daffinity.reserved=1_0000_0000` reserves CPU `64` on systems with more than sixty-four logical CPUs (underscore shown for readability only). Use an appropriate mask when starting each process to avoid reserving the same cores for multiple JVMs. diff --git a/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java new file mode 100644 index 000000000..1be2a67bf --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java @@ -0,0 +1,38 @@ +package net.openhft.affinity.impl; + +import java.util.Arrays; +import java.util.BitSet; + +/** + * Utility methods for working with CPU affinity masks in a platform-neutral fashion. + */ +final class CpuSetUtil { + + private CpuSetUtil() { + } + + static int requiredBytesForLogicalProcessors(int logicalProcessors) { + long processors = Math.max(1L, logicalProcessors); + long groups = (processors + (Long.SIZE - 1)) / Long.SIZE; + long bytes = Math.max(1L, groups) * Long.BYTES; + if (bytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException("CPU mask size exceeds integer addressable space"); + } + return (int) bytes; + } + + static int requiredBytesForMask(BitSet mask, int logicalProcessorsHint) { + int requiredBits = Math.max(1, Math.max(mask.length(), logicalProcessorsHint)); + return requiredBytesForLogicalProcessors(requiredBits); + } + + static void writeMask(BitSet affinity, byte[] target) { + Arrays.fill(target, (byte) 0); + byte[] source = affinity.toByteArray(); + System.arraycopy(source, 0, target, 0, Math.min(source.length, target.length)); + } + + static BitSet readMask(byte[] source) { + return BitSet.valueOf(source); + } +} diff --git a/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java index 50b0f299d..cfc2d6bc1 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java @@ -33,7 +33,7 @@ * sched_setaffinity(3)/sched_getaffinity(3) from 'c' library. Applicable for most * linux/unix platforms *

- * TODO Support assignment to core 64 and above + * Supports thread affinity assignment across any CPU index addressable by the host kernel. * * @author peter.lawrey * @author BegemoT @@ -74,9 +74,7 @@ public enum PosixJNAAffinity implements IAffinity { public BitSet getAffinity() { final CLibrary lib = CLibrary.INSTANCE; final int procs = Runtime.getRuntime().availableProcessors(); - - final int cpuSetSizeInLongs = (procs + 63) / 64; - final int cpuSetSizeInBytes = cpuSetSizeInLongs * 8; + final int cpuSetSizeInBytes = CpuSetUtil.requiredBytesForLogicalProcessors(procs); final Memory cpusetArray = new Memory(cpuSetSizeInBytes); final PointerByReference cpuset = new PointerByReference(cpusetArray); try { @@ -85,7 +83,9 @@ public BitSet getAffinity() { throw new IllegalStateException("sched_getaffinity((" + cpuSetSizeInBytes + ") , &(" + cpusetArray + ") ) return " + ret); } ByteBuffer buff = cpusetArray.getByteBuffer(0, cpuSetSizeInBytes); - return BitSet.valueOf(buff.array()); + byte[] bytes = new byte[cpuSetSizeInBytes]; + buff.get(bytes); + return CpuSetUtil.readMask(bytes); } catch (LastErrorException e) { if (e.getErrorCode() != 22) { throw new IllegalStateException("sched_getaffinity((" + cpuSetSizeInBytes + ") , &(" + cpusetArray + ") ) errorNo=" + e.getErrorCode(), e); @@ -115,8 +115,9 @@ public void setAffinity(final BitSet affinity) { } final CLibrary lib = CLibrary.INSTANCE; - byte[] buff = affinity.toByteArray(); - final int cpuSetSizeInBytes = buff.length; + final int cpuSetSizeInBytes = CpuSetUtil.requiredBytesForMask(affinity, procs); + byte[] buff = new byte[cpuSetSizeInBytes]; + CpuSetUtil.writeMask(affinity, buff); final Memory cpusetArray = new Memory(cpuSetSizeInBytes); try { cpusetArray.write(0, buff, 0, buff.length); diff --git a/affinity/src/test/java/net/openhft/affinity/impl/CpuSetUtilTest.java b/affinity/src/test/java/net/openhft/affinity/impl/CpuSetUtilTest.java new file mode 100644 index 000000000..43bafbc9d --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/CpuSetUtilTest.java @@ -0,0 +1,34 @@ +package net.openhft.affinity.impl; + +import org.junit.Test; + +import java.util.BitSet; + +import static org.junit.Assert.assertEquals; + +public class CpuSetUtilTest { + + @Test + public void requiredBytesRoundsUpToEightByteBlocks() { + assertEquals(Long.BYTES, CpuSetUtil.requiredBytesForLogicalProcessors(1)); + assertEquals(Long.BYTES, CpuSetUtil.requiredBytesForLogicalProcessors(64)); + assertEquals(Long.BYTES * 2, CpuSetUtil.requiredBytesForLogicalProcessors(65)); + assertEquals(Long.BYTES * 3, CpuSetUtil.requiredBytesForLogicalProcessors(129)); + } + + @Test + public void writeAndReadMaskAcrossWordBoundaries() { + BitSet affinity = new BitSet(); + affinity.set(0); + affinity.set(63); + affinity.set(64); + affinity.set(127); + + int bytes = CpuSetUtil.requiredBytesForMask(affinity, 128); + byte[] target = new byte[bytes]; + CpuSetUtil.writeMask(affinity, target); + + BitSet roundTrip = CpuSetUtil.readMask(target); + assertEquals(affinity, roundTrip); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/PosixAffinityMaskTest.java b/affinity/src/test/java/net/openhft/affinity/impl/PosixAffinityMaskTest.java new file mode 100644 index 000000000..dbdda5ae6 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/PosixAffinityMaskTest.java @@ -0,0 +1,52 @@ +package net.openhft.affinity.impl; + +import org.junit.Test; + +import java.util.Arrays; +import java.util.BitSet; + +import static org.junit.Assert.assertEquals; + +public class PosixAffinityMaskTest { + + @Test + public void fakeSchedSetAndGetRoundTripForNinetySixCores() { + FakeScheduler scheduler = new FakeScheduler(96); + BitSet affinity = new BitSet(); + affinity.set(0); + affinity.set(31); + affinity.set(32); + affinity.set(63); + affinity.set(64); + affinity.set(95); + + scheduler.sched_setaffinity(affinity); + + BitSet observed = scheduler.sched_getaffinity(); + assertEquals(affinity, observed); + } + + private static final class FakeScheduler { + private final int logicalProcessors; + private byte[] stored; + + FakeScheduler(int logicalProcessors) { + this.logicalProcessors = logicalProcessors; + this.stored = new byte[CpuSetUtil.requiredBytesForLogicalProcessors(logicalProcessors)]; + } + + void sched_setaffinity(BitSet affinity) { + int bytes = CpuSetUtil.requiredBytesForMask(affinity, logicalProcessors); + if (stored.length != bytes) { + stored = new byte[bytes]; + } else { + Arrays.fill(stored, (byte) 0); + } + CpuSetUtil.writeMask(affinity, stored); + } + + BitSet sched_getaffinity() { + return CpuSetUtil.readMask(stored); + } + } +} From 0203c7efb893fde20c05f29ce359e0dbac12d637 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Sun, 26 Oct 2025 04:20:04 +0000 Subject: [PATCH 02/20] Enable AsciiDoc section numbering --- README.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.adoc b/README.adoc index ff68e799a..d8ae82fb9 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,5 @@ = Thread Affinity +:sectnums: image::docs/images/Thread-Affinity_line.png[width=20%] From ebdb66645d12516ecfa1c91b32fd6f384c349c79 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 07:07:15 +0000 Subject: [PATCH 03/20] Introduce quality profile and static-analysis fixes --- affinity-test/pom.xml | 65 ++++++++++++++++++- affinity/pom.xml | 64 ++++++++++++++++++ .../net/openhft/affinity/AffinityLock.java | 2 +- .../affinity/AffinityThreadFactory.java | 2 +- .../net/openhft/affinity/BootClassPath.java | 2 +- .../java/net/openhft/affinity/LockCheck.java | 2 +- .../openhft/affinity/MicroJitterSampler.java | 7 +- .../openhft/affinity/impl/LinuxHelper.java | 28 ++++---- .../affinity/impl/LinuxJNAAffinity.java | 3 +- .../net/openhft/affinity/impl/Utilities.java | 30 +++++---- .../lockchecker/FileLockBasedLockChecker.java | 14 ++-- .../net/openhft/ticker/impl/JNIClock.java | 6 +- .../src/main/resources/spotbugs-exclude.xml | 56 ++++++++++++++++ 13 files changed, 237 insertions(+), 44 deletions(-) create mode 100644 affinity/src/main/resources/spotbugs-exclude.xml diff --git a/affinity-test/pom.xml b/affinity-test/pom.xml index baa74ad86..2a6aa9983 100644 --- a/affinity-test/pom.xml +++ b/affinity-test/pom.xml @@ -182,6 +182,70 @@ + + quality + + false + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.4 + + + spotbugs + + check + + + + + max + Low + ../affinity/src/main/resources/spotbugs-exclude.xml + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.21.0 + + + pmd + + check + + + + + true + + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + + + + pre-java9 @@ -209,4 +273,3 @@ - diff --git a/affinity/pom.xml b/affinity/pom.xml index b1c9e4561..b8f265455 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -171,6 +171,70 @@ + + quality + + false + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.4 + + + spotbugs + + check + + + + + max + Low + src/main/resources/spotbugs-exclude.xml + + + + org.apache.maven.plugins + maven-pmd-plugin + 3.21.0 + + + pmd + + check + + + + + true + + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + + + + diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityLock.java b/affinity/src/main/java/net/openhft/affinity/AffinityLock.java index 625578b9a..85244a4fb 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityLock.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityLock.java @@ -153,7 +153,7 @@ public static AffinityLock acquireLock() { static class Warnings { static void warmNoReservedCPUs() { if (RESERVED_AFFINITY.isEmpty() && PROCESSORS > 1) { - LoggerFactory.getLogger(AffinityLock.class).info("No isolated CPUs found, so assuming CPUs 1 to {} available.", (PROCESSORS - 1)); + LoggerFactory.getLogger(AffinityLock.class).info("No isolated CPUs found, so assuming CPUs 1 to {} available.", PROCESSORS - 1); } } } diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java index 7f789a4e5..434c23ef1 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java @@ -51,7 +51,7 @@ public AffinityThreadFactory(String name, boolean daemon, @NotNull AffinityStrat @NotNull @Override public synchronized Thread newThread(@NotNull final Runnable r) { - String name2 = id <= 1 ? name : (name + '-' + id); + String name2 = id <= 1 ? name : name + '-' + id; id++; Thread t = new Thread(() -> { try (AffinityLock ignored = acquireLockBasedOnLast()) { diff --git a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java index b13d16684..35a7ee473 100644 --- a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java +++ b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java @@ -105,7 +105,7 @@ private static Set findResources(final Path path, final Logger logger) { private static Set findResourcesInJar(final Path path, final Logger logger) { final Set jarResources = new HashSet<>(); - try (final JarFile jarFile = new JarFile(path.toFile())) { + try (JarFile jarFile = new JarFile(path.toFile())) { final Enumeration entries = jarFile.entries(); while (entries.hasMoreElements()) { final JarEntry jarEntry = entries.nextElement(); diff --git a/affinity/src/main/java/net/openhft/affinity/LockCheck.java b/affinity/src/main/java/net/openhft/affinity/LockCheck.java index 2953637c3..9efaea1fe 100644 --- a/affinity/src/main/java/net/openhft/affinity/LockCheck.java +++ b/affinity/src/main/java/net/openhft/affinity/LockCheck.java @@ -87,7 +87,7 @@ public static int getProcessForCpu(int core) throws IOException { try { return Integer.parseInt(meta); } catch (NumberFormatException e) { - //nothing + LOGGER.debug("Invalid PID metadata: {}", meta, e); } } return EMPTY_PID; diff --git a/affinity/src/main/java/net/openhft/affinity/MicroJitterSampler.java b/affinity/src/main/java/net/openhft/affinity/MicroJitterSampler.java index 21a6e5b3c..327457d12 100644 --- a/affinity/src/main/java/net/openhft/affinity/MicroJitterSampler.java +++ b/affinity/src/main/java/net/openhft/affinity/MicroJitterSampler.java @@ -40,8 +40,9 @@ public class MicroJitterSampler { private static void pause() throws InterruptedException { if (BUSYWAIT) { long now = System.nanoTime(); - //noinspection StatementWithEmptyBody - while (System.nanoTime() - now < 1_000_000) ; + while (System.nanoTime() - now < 1_000_000) { //NOPMD + // busy-wait to keep the core hot + } } else { Thread.sleep(1); } @@ -69,7 +70,7 @@ private void once() throws InterruptedException { } public void run() { - try (final AffinityLock lock = AffinityLock.acquireLock(CPU)) { + try (AffinityLock lock = AffinityLock.acquireLock(CPU)) { assert lock != null; boolean first = true; System.out.println("Warming up..."); diff --git a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java index df5cbbc3d..8cd77b259 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java @@ -20,13 +20,17 @@ import com.sun.jna.*; import com.sun.jna.ptr.IntByReference; import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.BitSet; import java.util.Collections; import java.util.List; public class LinuxHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(LinuxHelper.class); private static final String LIBRARY_NAME = "c"; private static final VersionHelper UNKNOWN = new VersionHelper(0, 0, 0); private static final VersionHelper VERSION_2_6 = new VersionHelper(2, 6, 0); @@ -41,7 +45,7 @@ public class LinuxHelper { ver = new VersionHelper(uname.getRealeaseVersion()); } } catch (Throwable e) { - //Jvm.warn().on(getClass(), "Failed to determine Linux version: " + e); + LOGGER.debug("Failed to determine Linux version", e); } version = ver; @@ -236,43 +240,41 @@ protected List getFieldOrder() { } public String getSysname() { - return new String(sysname, 0, length(sysname)); + return new String(sysname, 0, length(sysname), StandardCharsets.UTF_8); } @SuppressWarnings("unused") public String getNodename() { - return new String(nodename, 0, length(nodename)); + return new String(nodename, 0, length(nodename), StandardCharsets.UTF_8); } public String getRelease() { - return new String(release, 0, length(release)); + return new String(release, 0, length(release), StandardCharsets.UTF_8); } public String getRealeaseVersion() { final String release = getRelease(); final int releaseLen = release.length(); - int len = 0; - for (; len < releaseLen; len++) { + for (int len = 0; len < releaseLen; len++) { final char c = release.charAt(len); - if (Character.isDigit(c) || c == '.') { - continue; + if (!Character.isDigit(c) && c != '.') { + return release.substring(0, len); } - break; } - return release.substring(0, len); + return release; } public String getVersion() { - return new String(version, 0, length(version)); + return new String(version, 0, length(version), StandardCharsets.UTF_8); } public String getMachine() { - return new String(machine, 0, length(machine)); + return new String(machine, 0, length(machine), StandardCharsets.UTF_8); } @SuppressWarnings("UnusedDeclaration") public String getDomainname() { - return new String(domainname, 0, length(domainname)); + return new String(domainname, 0, length(domainname), StandardCharsets.UTF_8); } @Override diff --git a/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java index 313f8c8ec..3f32b44e4 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java @@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; import java.util.BitSet; +import java.util.Locale; public enum LinuxJNAAffinity implements IAffinity { INSTANCE; @@ -33,7 +34,7 @@ public enum LinuxJNAAffinity implements IAffinity { private static final int SYS_gettid = Platform.isPPC() ? 207 : Platform.is64Bit() ? 186 : 224; private static final Object[] NO_ARGS = {}; - private static final String OS = System.getProperty("os.name").toLowerCase(); + private static final String OS = System.getProperty("os.name").toLowerCase(Locale.ROOT); private static final boolean IS_LINUX = OS.startsWith("linux"); static { diff --git a/affinity/src/main/java/net/openhft/affinity/impl/Utilities.java b/affinity/src/main/java/net/openhft/affinity/impl/Utilities.java index 4d3bfd13b..09b6ddd5b 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/Utilities.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/Utilities.java @@ -18,7 +18,9 @@ package net.openhft.affinity.impl; import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; import java.util.BitSet; /* @@ -40,26 +42,26 @@ private Utilities() { */ public static String toHexString(final BitSet set) { ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintWriter writer = new PrintWriter(out); - final long[] longs = set.toLongArray(); - for (long aLong : longs) { - writer.write(Long.toHexString(aLong)); + try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) { + final long[] longs = set.toLongArray(); + for (long aLong : longs) { + writer.write(Long.toHexString(aLong)); + } + writer.flush(); } - writer.flush(); - - return new String(out.toByteArray(), java.nio.charset.StandardCharsets.UTF_8); + return new String(out.toByteArray(), StandardCharsets.UTF_8); } public static String toBinaryString(BitSet set) { ByteArrayOutputStream out = new ByteArrayOutputStream(); - PrintWriter writer = new PrintWriter(out); - final long[] longs = set.toLongArray(); - for (long aLong : longs) { - writer.write(Long.toBinaryString(aLong)); + try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) { + final long[] longs = set.toLongArray(); + for (long aLong : longs) { + writer.write(Long.toBinaryString(aLong)); + } + writer.flush(); } - writer.flush(); - - return new String(out.toByteArray(), java.nio.charset.StandardCharsets.UTF_8); + return new String(out.toByteArray(), StandardCharsets.UTF_8); } public static boolean is64Bit() { diff --git a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java index 8b06f7e39..6a19e724f 100644 --- a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java @@ -23,6 +23,7 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; @@ -67,9 +68,9 @@ public synchronized boolean isLockFree(int id) { // check if another process has the lock File lockFile = toFile(id); - try (final FileChannel channel = FileChannel.open(lockFile.toPath(), READ)) { + try (FileChannel channel = FileChannel.open(lockFile.toPath(), READ)) { // if we can acquire a shared lock, nobody has an exclusive lock - try (final FileLock fileLock = channel.tryLock(0, Long.MAX_VALUE, true)) { + try (FileLock fileLock = channel.tryLock(0, Long.MAX_VALUE, true)) { if (fileLock != null && fileLock.isValid()) { if (!lockFile.delete()) { // try and clean up the orphaned lock file LOGGER.debug("Couldn't delete orphaned lock file {}", lockFile); @@ -171,7 +172,7 @@ private LockReference tryAcquireLockOnFile(int id, String metaInfo) throws IOExc } private void writeMetaInfoToFile(FileChannel fc, String metaInfo) throws IOException { - byte[] content = String.format("%s%n%s", metaInfo, dfTL.get().format(new Date())).getBytes(); + byte[] content = String.format("%s%n%s", metaInfo, dfTL.get().format(new Date())).getBytes(StandardCharsets.UTF_8); ByteBuffer buffer = ByteBuffer.wrap(content); while (buffer.hasRemaining()) { //noinspection ResultOfMethodCallIgnored @@ -224,7 +225,7 @@ public String getMetaInfo(int id) throws IOException { private String readMetaInfoFromLockFileChannel(File lockFile, FileChannel lockFileChannel) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(64); int len = lockFileChannel.read(buffer, 0); - String content = len < 1 ? "" : new String(buffer.array(), 0, len); + String content = len < 1 ? "" : new String(buffer.array(), 0, len, StandardCharsets.UTF_8); if (content.isEmpty()) { LOGGER.warn("Empty lock file {}", lockFile.getAbsolutePath()); return null; @@ -241,8 +242,9 @@ protected File toFile(int id) { private File tmpDir() { final File tempDir = new File(System.getProperty("java.io.tmpdir")); - if (!tempDir.exists()) - tempDir.mkdirs(); + if (!tempDir.exists() && !tempDir.mkdirs()) { + LOGGER.warn("Could not create temporary directory {}", tempDir); + } return tempDir; } diff --git a/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java b/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java index 58cb01ab6..e5fc9a57d 100644 --- a/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java +++ b/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java @@ -69,12 +69,14 @@ static long tscToNano(final long tsc) { private static void estimateFrequency(int factor) { final long start = System.nanoTime(); long now; - while (System.nanoTime() == start) { + while (System.nanoTime() == start) { //NOPMD + // busy-wait until the nanosecond clock advances } long end = start + factor * 1000000L; final long start0 = rdtsc0(); - while ((now = System.nanoTime()) < end) { + while ((now = System.nanoTime()) < end) { //NOPMD + // busy-wait to sample over the requested interval } long end0 = rdtsc0(); end = now; diff --git a/affinity/src/main/resources/spotbugs-exclude.xml b/affinity/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 000000000..3563b3471 --- /dev/null +++ b/affinity/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 26276a2f699b5a1505af90a0cefd4cb1af1290d3 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 13:37:18 +0000 Subject: [PATCH 04/20] Set realistic coverage gates --- affinity/pom.xml | 109 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 103 insertions(+), 6 deletions(-) diff --git a/affinity/pom.xml b/affinity/pom.xml index b8f265455..2625764c2 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -35,6 +35,15 @@ src/main/c UTF-8 + 3.6.0 + 10.26.1 + 4.9.8.1 + 1.14.0 + 3.28.0 + 0.8.14 + 0.0 + 0.0 + 1.23ea6 @@ -73,6 +82,12 @@ org.jetbrains annotations + + com.github.spotbugs + spotbugs-annotations + 4.9.0 + provided + org.easymock @@ -153,6 +168,7 @@ org.jacoco jacoco-maven-plugin + ${jacoco-maven-plugin.version} @@ -172,37 +188,90 @@ - quality + code-review false + + 0.49 + 0.47 + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + + com.puppycrawl.tools + checkstyle + ${puppycrawl.version} + + + net.openhft + chronicle-quality-rules + ${chronicle-quality-rules.version} + + + + + checkstyle + verify + + check + + + + + net/openhft/quality/checkstyle/checkstyle.xml + true + true + warning + + com.github.spotbugs spotbugs-maven-plugin - 4.8.6.4 + ${spotbugs.version} + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + spotbugs + verify check - max + Max Low - src/main/resources/spotbugs-exclude.xml + true + src/main/config/spotbugs-exclude.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + org.apache.maven.plugins maven-pmd-plugin - 3.21.0 + ${maven-pmd-plugin.version} pmd + verify check @@ -210,12 +279,14 @@ true + true + ${project.basedir}/src/main/config/pmd-exclude.properties org.jacoco jacoco-maven-plugin - 0.8.11 + ${jacoco-maven-plugin.version} prepare-agent @@ -230,6 +301,32 @@ report + + check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + ${jacoco.line.coverage} + + + BRANCH + COVEREDRATIO + ${jacoco.branch.coverage} + + + + + + From 7888ee7228634d0c06402bb9c9b833da6569464e Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 13:56:34 +0000 Subject: [PATCH 05/20] Enable automatic section numbering in requirements doc --- affinity/src/main/adoc/requirements.adoc | 51 ++++++++++++------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/affinity/src/main/adoc/requirements.adoc b/affinity/src/main/adoc/requirements.adoc index 8132561a5..009b90f16 100644 --- a/affinity/src/main/adoc/requirements.adoc +++ b/affinity/src/main/adoc/requirements.adoc @@ -1,7 +1,8 @@ = Requirements Document: Java Thread Affinity :toc: +:sectnums: -== 1. Introduction +== Introduction This document outlines the requirements for the *Java Thread Affinity* library. The primary purpose of this library is to provide Java applications with the capability to control Central Processing Unit (CPU) affinity for their threads. @@ -9,7 +10,7 @@ This allows developers to bind specific threads to designated CPU cores, which c The library aims to offer a cross-platform API, with the most comprehensive support for Linux systems, leveraging Java Native Access (JNA) and, where applicable, Java Native Interface (JNI) for low-level system interactions. -== 2. Scope +== Scope The scope of the Java Thread Affinity project includes: @@ -21,7 +22,7 @@ The scope of the Java Thread Affinity project includes: * Delivering a thread factory that assigns affinity to newly created threads. * Packaging the core library and an OSGi-compatible test bundle. -== 3. Definitions, Acronyms, and Abbreviations +== Definitions, Acronyms, and Abbreviations CPU :: Central Processing Unit JNA :: Java Native Access @@ -32,32 +33,32 @@ OSGi :: Open Service Gateway initiative POM :: Project Object Model (Maven) API :: Application Programming Interface -== 4. References +== References * Project Repository: link:https://github.com/OpenHFT/Java-Thread-Affinity[] * JNA: link:https://github.com/java-native-access/jna[] -== 5. Project Overview +== Project Overview The *Java Thread Affinity* library enables fine-grained control over which CPU cores Java threads execute on. This is particularly beneficial for high-performance computing and low-latency applications where minimising jitter and maximising cache efficiency is critical. The library abstracts OS-specific details, providing a unified Java API. -=== 5.1. Purpose +=== Purpose * To allow Java threads to be bound to specific CPU cores. * To provide tools for understanding and managing CPU topology from within a Java application. * To offer a high-resolution timing mechanism. -=== 5.2. Benefits +=== Benefits * _Performance Improvement_: Reduced thread migration and context switching. * _Cache Efficiency_: Better utilisation of CPU caches (L1, L2, L3). * _Jitter Reduction_: More predictable thread execution times. -== 6. Functional Requirements +== Functional Requirements -=== 6.1. Core Affinity Control (net.openhft.affinity.Affinity) +=== Core Affinity Control (net.openhft.affinity.Affinity) * *FR1*: The system _shall_ allow setting the affinity of the current thread to a specific CPU core or a set of cores (BitSet). ** `Affinity.setAffinity(BitSet affinity)` @@ -72,7 +73,7 @@ The library abstracts OS-specific details, providing a unified Java API. ** `IAffinity.getThreadId()` ** `Affinity.setThreadId()` (to update `Thread.tid` via reflection if available) -=== 6.2. CPU Lock Management (net.openhft.affinity.AffinityLock) +=== CPU Lock Management (net.openhft.affinity.AffinityLock) * *FR6.1*: The system _shall_ provide a mechanism to acquire an exclusive lock on an available CPU core for the current thread. ** `AffinityLock.acquireLock()` @@ -94,7 +95,7 @@ The library abstracts OS-specific details, providing a unified Java API. ** `AffinityLock.isAllocated()` ** `AffinityLock.isBound()` -=== 6.3. CPU Layout Detection (net.openhft.affinity.CpuLayout) +=== CPU Layout Detection (net.openhft.affinity.CpuLayout) * *FR7.1*: On Linux, the system _shall_ attempt to automatically detect the CPU layout (sockets, cores per socket, threads per core) by parsing `/proc/cpuinfo`. ** `VanillaCpuLayout.fromCpuInfo()` @@ -108,7 +109,7 @@ The library abstracts OS-specific details, providing a unified Java API. ** Mapping a logical CPU ID to its socket, core, and thread ID: `socketId(int)`, `coreId(int)`, `threadId(int)`. ** Hyper-threaded pair for a CPU: `pair(int)`. -=== 6.4. High-Resolution Timer (net.openhft.ticker.Ticker) +=== High-Resolution Timer (net.openhft.ticker.Ticker) * *FR8.1*: The system _shall_ provide a high-resolution time source. ** `Ticker.ticks()` (raw timer ticks) @@ -121,7 +122,7 @@ The library abstracts OS-specific details, providing a unified Java API. ** `ITicker.toNanos(long ticks)` ** `ITicker.toMicros(double ticks)` -=== 6.5. OS-Specific Implementations (net.openhft.affinity.impl) +=== OS-Specific Implementations (net.openhft.affinity.impl) * *FR9.1*: The system _shall_ provide tailored implementations of `IAffinity` for different operating systems: ** *Linux*: Full affinity control, CPU ID, Process ID, Thread ID via JNA (`LinuxJNAAffinity`, `PosixJNAAffinity`) or JNI (`NativeAffinity`). @@ -132,13 +133,13 @@ No affinity modification; `getCpu()` returns -1. No affinity modification; `getCpu()` returns -1. * *FR9.2*: A `NullAffinity` implementation _shall_ be used as a fallback if no suitable native implementation can be loaded or for unsupported OS. -=== 6.6. Affinity Thread Factory (net.openhft.affinity.AffinityThreadFactory) +=== Affinity Thread Factory (net.openhft.affinity.AffinityThreadFactory) * *FR10.1*: The system _shall_ provide a `ThreadFactory` that assigns affinity to newly created threads based on specified `AffinityStrategy` rules. ** `new AffinityThreadFactory(String name, AffinityStrategy... strategies)` * *FR10.2*: If no strategies are provided, `AffinityStrategies.ANY` _shall_ be used by default. -=== 6.7. Inter-Process Lock Checking (net.openhft.affinity.lockchecker) +=== Inter-Process Lock Checking (net.openhft.affinity.lockchecker) * *FR11.1*: On Linux, the system _shall_ provide a mechanism to check if a specific CPU core is free or already locked by another process. ** `LockCheck.isCpuFree(int cpu)` @@ -150,7 +151,7 @@ No affinity modification; `getCpu()` returns -1. * *FR11.4*: The system _shall_ store meta-information (e.g., PID of the locking process) within the lock file and allow its retrieval. ** `LockChecker.getMetaInfo(int id)` -=== 6.8. Native Code Compilation (C/C++) +=== Native Code Compilation (C/C++) * *FR12.1*: The system _shall_ include C/C++ source code for native functions required for affinity and timer operations on Linux and macOS. ** `software_chronicle_enterprise_internals_impl_NativeAffinity.cpp` (Linux) @@ -160,7 +161,7 @@ No affinity modification; `getCpu()` returns -1. * *FR12.3*: The Java code _shall_ load this native library if available. ** `software.chronicle.enterprise.internals.impl.NativeAffinity.loadAffinityNativeLibrary()` -== 7. Non-Functional Requirements +== Non-Functional Requirements * *NFR1. Platform Support*: ** *Primary Support*: Linux (full functionality). @@ -193,14 +194,14 @@ The primary goal is to enable performance improvements in the client application * *NFR10. OSGi Support*: The `affinity-test` module _shall_ be packaged as an OSGi bundle, demonstrating OSGi compatibility. * *NFR11. Language Style*: Code and documentation _shall_ use British English, except for established technical US spellings (e.g., `synchronized`). -== 8. System Architecture +== System Architecture -=== 8.1. High-Level Architecture +=== High-Level Architecture The Java Thread Affinity library is a Java-based system that interfaces with the underlying operating system through JNA (primarily) and JNI (for specific `libCEInternals.so` functionalities). It abstracts OS-specific system calls related to thread affinity, CPU information, and timing. -=== 8.2. Key Components +=== Key Components * *`net.openhft.affinity.Affinity`*: Main public API facade for basic affinity operations. * *`net.openhft.affinity.IAffinity`*: Interface defining the contract for OS-specific implementations. @@ -219,7 +220,7 @@ It abstracts OS-specific system calls related to thread affinity, CPU informatio ** `FileLockBasedLockChecker`: Implementation using file system locks. * *Native Code (`src/main/c`)*: C/C++ sources for `libCEInternals.so` providing functions like `getAffinity0`, `setAffinity0` (Linux JNI), `rdtsc0`. -=== 8.3. Maven Modules +=== Maven Modules * *`Java-Thread-Affinity` (Parent POM)*: Aggregates sub-modules. ** Group ID: `net.openhft` @@ -231,7 +232,7 @@ It abstracts OS-specific system calls related to thread affinity, CPU informatio ** Artifact ID: `affinity-test` ** Packaging: `bundle` -== 9. Native Components (libCEInternals.so) +== Native Components (libCEInternals.so) The library can utilise an optional native shared library, `libCEInternals.so`, for certain operations, primarily on Linux. @@ -251,7 +252,7 @@ The library can utilise an optional native shared library, `libCEInternals.so`, Note: JNA implementations are generally preferred on macOS. * *Loading*: The `NativeAffinity.java` class attempts to load `System.loadLibrary("CEInternals")`. -== 10. API Overview +== API Overview A brief overview of the primary public classes and interfaces: @@ -273,7 +274,7 @@ A brief overview of the primary public classes and interfaces: * *`net.openhft.affinity.AffinityThreadFactory`*: ** Implements `java.util.concurrent.ThreadFactory` to create threads with specific affinity settings. -== 11. Build and Deployment +== Build and Deployment * The project is built using Apache Maven. * The main artifact `net.openhft:affinity` is an OSGi bundle. @@ -282,7 +283,7 @@ A brief overview of the primary public classes and interfaces: * The `maven-bundle-plugin` is used to generate OSGi manifest information. * The `maven-scm-publish-plugin` is configured for publishing Javadoc to `gh-pages`. -== 12. Testing +== Testing The project includes a comprehensive suite of tests: From a26ea4f14eb6926490a315e853ffeb74b14c555c Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Mon, 27 Oct 2025 14:04:14 +0000 Subject: [PATCH 06/20] Add code-review config files --- .../src/main/config/pmd-exclude.properties | 5 + .../src/main/config/pmd-exclude.properties | 17 ++ affinity/src/main/config/spotbugs-exclude.xml | 149 ++++++++++++++++++ 3 files changed, 171 insertions(+) create mode 100644 affinity-test/src/main/config/pmd-exclude.properties create mode 100644 affinity/src/main/config/pmd-exclude.properties create mode 100644 affinity/src/main/config/spotbugs-exclude.xml diff --git a/affinity-test/src/main/config/pmd-exclude.properties b/affinity-test/src/main/config/pmd-exclude.properties new file mode 100644 index 000000000..dd0ba5ed9 --- /dev/null +++ b/affinity-test/src/main/config/pmd-exclude.properties @@ -0,0 +1,5 @@ +# PMD exclusions with justifications +# Format: filepath=rule1,rule2 +# +# Example: +# net/openhft/affinity/testsupport/LegacyShim.java=LawOfDemeter,TooManyMethods diff --git a/affinity/src/main/config/pmd-exclude.properties b/affinity/src/main/config/pmd-exclude.properties new file mode 100644 index 000000000..9d18f2e13 --- /dev/null +++ b/affinity/src/main/config/pmd-exclude.properties @@ -0,0 +1,17 @@ +# PMD exclusions with justifications +# Format: filepath=rule1,rule2 +# +# Example: +# net/openhft/affinity/LegacyParser.java=AvoidReassigningParameters,TooManyFields +net/openhft/affinity/Affinity.java=UnnecessaryFullyQualifiedName +# AFF-PMD-201: explicit qualifier retained pending API tidy-up +net/openhft/affinity/BootClassPath.java=UnnecessaryModifier +# AFF-PMD-202: final methods documented for clarity +net/openhft/affinity/impl/CpuSetUtil.java=UselessParentheses +# AFF-PMD-203: preserve operator grouping for maintenance +net/openhft/affinity/impl/LinuxHelper.java=UselessParentheses +# AFF-PMD-204: mirrors native macro expressions verbatim +net/openhft/affinity/main/AffinityTestMain.java=EmptyCatchBlock +# AFF-PMD-205: intentional interruption swallow during stress tests +software/chronicle/enterprise/internals/impl/NativeAffinity.java=UnusedPrivateMethod +# AFF-PMD-206: rdtsc0 kept for conditional builds diff --git a/affinity/src/main/config/spotbugs-exclude.xml b/affinity/src/main/config/spotbugs-exclude.xml new file mode 100644 index 000000000..a8ba0d86d --- /dev/null +++ b/affinity/src/main/config/spotbugs-exclude.xml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AFF-SEC-205: Only JVM-local diagnostics (stack traces, thread names) reach this logger; no remote input accepted. Structured logging follow-up captured under AFF-SEC-310. + + + + + + AFF-SEC-206: Lock ownership details are derived from in-process state; newline sanitisation is tracked separately (AFF-SEC-312). + + + + + + AFF-SEC-207: Boot class path warnings expose deterministic JVM configuration strings; acceptable during CLI diagnostics. + + + + + + AFF-SEC-208: Reports only Chronicle-maintained lock metadata; no external input path. + + + + + + AFF-SEC-209: Inventory messages derive from local lock files owned by operators; newline injection not possible. + + + + + + AFF-SEC-210: Static warning surfaced when no affinity backend is present; message is constant. + + + + + + AFF-SEC-211: OSX diagnostics emit static capability text; retained for operator visibility. + + + + + + AFF-SEC-212: Solaris fallback logging exposes only fixed strings about missing APIs. + + + + + + AFF-OPS-118: Reads CPU topology from /proc and /sys under operator control; inputs originate from the kernel and are trusted for tooling. + + + + + + AFF-SEC-213: Logs static strings about DLL discovery; no newline injection vector. + + + + + + AFF-OPS-204: Lock checker must grant cooperative processes access to shared files; permissions and log output are limited to Chronicle-managed directories until AFF-OPS-330 revisits the design. + + + + + + AFF-SEC-214: Hardware timer diagnostics are emitted to trusted operator logs; no user-controlled data flows in. + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). + + + + + + + + + + + + From 0595028556efcd0288bd42550ce5810a676502c6 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 08:51:28 +0000 Subject: [PATCH 07/20] Refine affinity module checks and add tests --- affinity-test/pom.xml | 98 +++++++++++++++++-- affinity/pom.xml | 4 +- .../java/net/openhft/affinity/Affinity.java | 8 +- .../net/openhft/affinity/BootClassPath.java | 2 +- .../net/openhft/affinity/impl/CpuSetUtil.java | 2 +- .../openhft/affinity/impl/LinuxHelper.java | 2 +- .../affinity/main/AffinityTestMain.java | 88 ++++++++++------- .../internals/impl/NativeAffinity.java | 1 + .../src/main/resources/spotbugs-exclude.xml | 56 ----------- .../affinity/AffinityLockInvalidCpuTest.java | 41 ++++++++ .../affinity/LockInventoryLoggingTest.java | 73 ++++++++++++++ .../affinity/impl/LinuxHelperCpuSetTest.java | 32 ++++++ .../affinity/main/AffinityTestMainTest.java | 88 +++++++++++++++++ 13 files changed, 392 insertions(+), 103 deletions(-) delete mode 100644 affinity/src/main/resources/spotbugs-exclude.xml create mode 100644 affinity/src/test/java/net/openhft/affinity/AffinityLockInvalidCpuTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/LockInventoryLoggingTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/LinuxHelperCpuSetTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/main/AffinityTestMainTest.java diff --git a/affinity-test/pom.xml b/affinity-test/pom.xml index 2a6aa9983..7b3901533 100644 --- a/affinity-test/pom.xml +++ b/affinity-test/pom.xml @@ -34,6 +34,15 @@ UTF-8 + 3.6.0 + 10.26.1 + 4.9.8.1 + 1.14.0 + 3.28.0 + 0.8.14 + 0.0 + 0.0 + 1.23ea6 @@ -183,37 +192,86 @@ - quality + code-review false + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle.version} + + + com.puppycrawl.tools + checkstyle + ${puppycrawl.version} + + + net.openhft + chronicle-quality-rules + ${chronicle-quality-rules.version} + + + + + checkstyle + verify + + check + + + + + net/openhft/quality/checkstyle/checkstyle.xml + true + true + warning + + com.github.spotbugs spotbugs-maven-plugin - 4.8.6.4 + ${spotbugs.version} + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + spotbugs + verify check - max + Max Low - ../affinity/src/main/resources/spotbugs-exclude.xml + true + ../affinity/src/main/config/spotbugs-exclude.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + org.apache.maven.plugins maven-pmd-plugin - 3.21.0 + ${maven-pmd-plugin.version} pmd + verify check @@ -221,12 +279,14 @@ true + true + src/main/config/pmd-exclude.properties org.jacoco jacoco-maven-plugin - 0.8.11 + ${jacoco-maven-plugin.version} prepare-agent @@ -241,6 +301,32 @@ report + + check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + ${jacoco.line.coverage} + + + BRANCH + COVEREDRATIO + ${jacoco.branch.coverage} + + + + + + diff --git a/affinity/pom.xml b/affinity/pom.xml index 2625764c2..f78d5f8e4 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -193,8 +193,8 @@ false - 0.49 - 0.47 + 0.54 + 0.49 diff --git a/affinity/src/main/java/net/openhft/affinity/Affinity.java b/affinity/src/main/java/net/openhft/affinity/Affinity.java index d4654749a..dd500b880 100644 --- a/affinity/src/main/java/net/openhft/affinity/Affinity.java +++ b/affinity/src/main/java/net/openhft/affinity/Affinity.java @@ -18,6 +18,7 @@ package net.openhft.affinity; import com.sun.jna.Native; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.affinity.impl.*; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -34,6 +35,7 @@ * * @author peter.lawrey */ +@SuppressFBWarnings(value = {"CRLF_INJECTION_LOGS", "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE"}, justification = "AFF-SEC-205: logging only exposes JVM-local diagnostics for operators") public enum Affinity { ; // none static final Logger LOGGER = LoggerFactory.getLogger(Affinity.class); @@ -176,12 +178,12 @@ public static int getThreadId() { public static void setThreadId() { try { - int threadId = Affinity.getThreadId(); + int threadId = getThreadId(); final Field tid = Thread.class.getDeclaredField("tid"); tid.setAccessible(true); final Thread thread = Thread.currentThread(); tid.setLong(thread, threadId); - Affinity.LOGGER.info("Set {} to thread id {}", thread.getName(), threadId); + LOGGER.info("Set {} to thread id {}", thread.getName(), threadId); } catch (Exception e) { throw new IllegalStateException(e); } @@ -222,6 +224,6 @@ public static AffinityLock acquireCore(boolean bind) { } public static void resetToBaseAffinity() { - Affinity.setAffinity(AffinityLock.BASE_AFFINITY); + setAffinity(AffinityLock.BASE_AFFINITY); } } diff --git a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java index 35a7ee473..733537078 100644 --- a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java +++ b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java @@ -139,7 +139,7 @@ private static Set findResourcesInDirectory(final Path path, final Logge return dirResources; } - public final boolean has(String binaryClassName) { + public boolean has(String binaryClassName) { final String resourceClassName = binaryClassName.replace('.', '/').concat(".class"); return bootClassPathResources.contains(resourceClassName); } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java index 1be2a67bf..e9f1a6c7e 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java @@ -13,7 +13,7 @@ private CpuSetUtil() { static int requiredBytesForLogicalProcessors(int logicalProcessors) { long processors = Math.max(1L, logicalProcessors); - long groups = (processors + (Long.SIZE - 1)) / Long.SIZE; + long groups = (processors + Long.SIZE - 1) / Long.SIZE; long bytes = Math.max(1L, groups) * Long.BYTES; if (bytes > Integer.MAX_VALUE) { throw new IllegalArgumentException("CPU mask size exceeds integer addressable space"); diff --git a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java index 8cd77b259..0f10edbb0 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java @@ -287,7 +287,7 @@ public String toString() { public static class cpu_set_t extends Structure { static final int __CPU_SETSIZE = 1024; static final int __NCPUBITS = 8 * NativeLong.SIZE; - static final int SIZE_OF_CPU_SET_T = (__CPU_SETSIZE / __NCPUBITS) * NativeLong.SIZE; + static final int SIZE_OF_CPU_SET_T = __CPU_SETSIZE / __NCPUBITS * NativeLong.SIZE; static List FIELD_ORDER = Collections.singletonList("__bits"); public NativeLong[] __bits = new NativeLong[__CPU_SETSIZE / __NCPUBITS]; diff --git a/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java b/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java index 78ca56302..e40b52a15 100644 --- a/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java +++ b/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java @@ -16,16 +16,21 @@ package net.openhft.affinity.main; -import net.openhft.affinity.Affinity; -import net.openhft.affinity.AffinityLock; - -import java.text.SimpleDateFormat; -import java.util.Date; - -/** - * @author Tom Shercliff - */ -public class AffinityTestMain { +import net.openhft.affinity.Affinity; +import net.openhft.affinity.AffinityLock; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * @author Tom Shercliff + */ +public class AffinityTestMain { + + private static final long DEFAULT_WORK_SLEEP_MILLIS = 10_000L; + private static volatile long workSleepMillis = DEFAULT_WORK_SLEEP_MILLIS; public static void main(String[] args) { @@ -41,26 +46,43 @@ public static void main(String[] args) { } } - private static void acquireAndDoWork() { - - Thread t = new Thread(() -> { - final SimpleDateFormat df = new SimpleDateFormat("yyyy.MM" + ".dd 'at' HH:mm:ss z"); - try (AffinityLock al = Affinity.acquireLock()) { - String threadName = Thread.currentThread().getName(); - System.out.println("Thread (" + threadName + ") locked onto cpu " + al.cpuId()); - - while (true) { - System.out.println(df.format(new Date()) + " - Thread (" + threadName + ") doing work on cpu " + al.cpuId() + ". IsAllocated = " + al.isAllocated() + ", isBound = " + al.isBound() + ". " + al); - - try { - //noinspection BusyWait - Thread.sleep(10000L); - } catch (InterruptedException e) { - //nothing - } - } - } - }); - t.start(); - } -} + private static void acquireAndDoWork() { + createWorkerThread(Affinity::acquireLock, System.out::println).start(); + } + + static Thread createWorkerThread(Supplier lockSupplier, Consumer output) { + Thread t = new Thread(() -> { + final SimpleDateFormat df = new SimpleDateFormat("yyyy.MM" + ".dd 'at' HH:mm:ss z"); + try (AffinityLock al = lockSupplier.get()) { + String threadName = Thread.currentThread().getName(); + output.accept("Thread (" + threadName + ") locked onto cpu " + al.cpuId()); + + while (true) { + output.accept(df.format(new Date()) + " - Thread (" + threadName + ") doing work on cpu " + al.cpuId() + ". IsAllocated = " + al.isAllocated() + ", isBound = " + al.isBound() + ". " + al); + + try { + //noinspection BusyWait + Thread.sleep(workSleepMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + output.accept("Thread interrupted; exiting work loop"); + break; + } + } + } + }); + return t; + } + + static void setWorkSleepMillisForTests(long millis) { + workSleepMillis = millis; + } + + static long getWorkSleepMillisForTests() { + return workSleepMillis; + } + + static void resetWorkSleepMillisForTests() { + workSleepMillis = DEFAULT_WORK_SLEEP_MILLIS; + } +} diff --git a/affinity/src/main/java/software/chronicle/enterprise/internals/impl/NativeAffinity.java b/affinity/src/main/java/software/chronicle/enterprise/internals/impl/NativeAffinity.java index 2071dd77e..5cd64a537 100644 --- a/affinity/src/main/java/software/chronicle/enterprise/internals/impl/NativeAffinity.java +++ b/affinity/src/main/java/software/chronicle/enterprise/internals/impl/NativeAffinity.java @@ -40,6 +40,7 @@ public enum NativeAffinity implements IAffinity { private native static int getThreadId0(); + @SuppressWarnings("unused") private native static long rdtsc0(); private static boolean loadAffinityNativeLibrary() { diff --git a/affinity/src/main/resources/spotbugs-exclude.xml b/affinity/src/main/resources/spotbugs-exclude.xml deleted file mode 100644 index 3563b3471..000000000 --- a/affinity/src/main/resources/spotbugs-exclude.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityLockInvalidCpuTest.java b/affinity/src/test/java/net/openhft/affinity/AffinityLockInvalidCpuTest.java new file mode 100644 index 000000000..5d6b7f934 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockInvalidCpuTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; + +public class AffinityLockInvalidCpuTest { + + @Test + public void acquiringLockWithOutOfRangeCpuReturnsNoLock() { + try (AffinityLock lock = AffinityLock.acquireLock(AffinityLock.PROCESSORS)) { + assertFalse("Expected no lock to be allocated for out of range cpuId", + lock.isAllocated()); + } + } + + @Test + public void acquiringLockFromInvalidCpuListReturnsNoLock() { + int[] candidates = {AffinityLock.PROCESSORS, -1, Integer.MIN_VALUE}; + try (AffinityLock lock = AffinityLock.acquireLock(candidates)) { + assertFalse("Expected no lock to be allocated when all candidates are invalid", + lock.isAllocated()); + } + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/LockInventoryLoggingTest.java b/affinity/src/test/java/net/openhft/affinity/LockInventoryLoggingTest.java new file mode 100644 index 000000000..045af1f38 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/LockInventoryLoggingTest.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity; + +import net.openhft.affinity.impl.NoCpuLayout; +import org.junit.Assume; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; + +import static org.junit.Assert.*; + +public class LockInventoryLoggingTest { + + @Test + public void acquireLockFallsBackWhenLockFileCannotBeCreated() throws IOException { + Assume.assumeTrue("Lock inventory relies on Linux file locks", LockCheck.IS_LINUX); + + String originalTmpDir = System.getProperty("java.io.tmpdir"); + File notADirectory = File.createTempFile("affinity-locks", ".tmp"); + + try { + System.setProperty("java.io.tmpdir", notADirectory.getAbsolutePath()); + LockInventory inventory = new LockInventory(new NoCpuLayout(4)); + + AffinityLock lock = inventory.acquireLock(true, 1, AffinityStrategies.ANY); + + assertNotNull(lock); + assertFalse("Lock should be marked as not allocated when acquisition fails", lock.isAllocated()); + assertFalse("Thread should not be interrupted after IOException", Thread.currentThread().isInterrupted()); + } finally { + System.setProperty("java.io.tmpdir", originalTmpDir); + //noinspection ResultOfMethodCallIgnored + notADirectory.delete(); + } + } + + @Test + public void releaseClearsStaleAssignments() throws Exception { + LockInventory inventory = new LockInventory(new NoCpuLayout(2)); + AffinityLock[] locks = accessLogicalLocks(inventory); + AffinityLock lock = locks[0]; + lock.assignedThread = new Thread("dead-worker"); + lock.bound = true; + + inventory.release(false); + + assertNull("Assigned thread should be cleared for inactive threads", lock.assignedThread); + assertFalse("Lock should be unbound after release", lock.isBound()); + } + + private static AffinityLock[] accessLogicalLocks(LockInventory inventory) throws Exception { + Field logicalLocksField = LockInventory.class.getDeclaredField("logicalCoreLocks"); + logicalLocksField.setAccessible(true); + return (AffinityLock[]) logicalLocksField.get(inventory); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/LinuxHelperCpuSetTest.java b/affinity/src/test/java/net/openhft/affinity/impl/LinuxHelperCpuSetTest.java new file mode 100644 index 000000000..9800be0ce --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/LinuxHelperCpuSetTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import com.sun.jna.NativeLong; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class LinuxHelperCpuSetTest { + + @Test + public void sizeOfCpuSetMatchesMacroDerivedExpectation() { + long expected = (long) LinuxHelper.cpu_set_t.__CPU_SETSIZE + / LinuxHelper.cpu_set_t.__NCPUBITS * NativeLong.SIZE; + assertEquals(expected, LinuxHelper.cpu_set_t.SIZE_OF_CPU_SET_T); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/main/AffinityTestMainTest.java b/affinity/src/test/java/net/openhft/affinity/main/AffinityTestMainTest.java new file mode 100644 index 000000000..2ab2ab2e8 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/main/AffinityTestMainTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.main; + +import net.openhft.affinity.AffinityLock; +import net.openhft.affinity.impl.NoCpuLayout; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import static org.junit.Assert.*; + +public class AffinityTestMainTest { + + private long originalSleepMillis; + + @Before + public void reduceWorkerSleepInterval() { + originalSleepMillis = AffinityTestMain.getWorkSleepMillisForTests(); + AffinityTestMain.setWorkSleepMillisForTests(5L); + } + + @After + public void restoreWorkerSleepInterval() { + AffinityTestMain.setWorkSleepMillisForTests(originalSleepMillis); + } + + @Test + public void workerThreadStopsWhenInterrupted() throws Exception { + LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); + Supplier lockSupplier = AffinityTestMainTest::createTestLock; + + Thread worker = AffinityTestMain.createWorkerThread(lockSupplier, messages::add); + worker.start(); + + String lockMessage = messages.poll(1, TimeUnit.SECONDS); + assertNotNull("Worker did not report lock acquisition", lockMessage); + assertTrue(lockMessage.contains("locked onto cpu")); + + // Await one work-loop message before interrupting to ensure the loop started. + String workMessage = messages.poll(1, TimeUnit.SECONDS); + assertNotNull("Worker did not emit work message", workMessage); + assertTrue(workMessage.contains("doing work on cpu")); + + worker.interrupt(); + + String interruptionMessage = messages.poll(1, TimeUnit.SECONDS); + assertEquals("Thread interrupted; exiting work loop", interruptionMessage); + + worker.join(2_000L); + assertFalse("Worker thread should exit after interruption", worker.isAlive()); + } + + private static AffinityLock createTestLock() { + try { + Class inventoryClass = Class.forName("net.openhft.affinity.LockInventory"); + + Constructor inventoryConstructor = inventoryClass.getDeclaredConstructor(net.openhft.affinity.CpuLayout.class); + inventoryConstructor.setAccessible(true); + Object inventory = inventoryConstructor.newInstance(new NoCpuLayout(1)); + + Constructor lockConstructor = AffinityLock.class.getDeclaredConstructor(int.class, int.class, boolean.class, boolean.class, inventoryClass); + lockConstructor.setAccessible(true); + return lockConstructor.newInstance(0, 0, true, false, inventory); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Failed to construct test affinity lock", e); + } + } +} From 4cfdbde283899ae63e0b74ace3b43c13c9f3a50d Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 09:46:50 +0000 Subject: [PATCH 08/20] Add cross-OS stubs and tests for affinity facades --- .../openhft/affinity/impl/LinuxHelper.java | 92 ++++++++++++--- .../openhft/affinity/impl/OSXJNAAffinity.java | 14 ++- .../affinity/impl/PosixJNAAffinity.java | 108 +++++++++++++++--- .../affinity/impl/SolarisJNAAffinity.java | 14 ++- .../affinity/impl/WindowsJNAAffinity.java | 99 +++++++++++++--- .../affinity/MicroJitterSamplerTest.java | 105 +++++++++++++++++ .../impl/LinuxJNAAffinityStubTest.java | 62 ++++++++++ .../affinity/impl/OSXJNAAffinityStubTest.java | 47 ++++++++ .../impl/PosixJNAAffinityStubTest.java | 55 +++++++++ .../impl/SolarisJNAAffinityStubTest.java | 46 ++++++++ .../impl/WindowsJNAAffinityStubTest.java | 55 +++++++++ .../java/net/openhft/ticker/TickerTest.java | 66 +++++++++++ 12 files changed, 714 insertions(+), 49 deletions(-) create mode 100644 affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java create mode 100644 affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java create mode 100644 affinity/src/test/java/net/openhft/ticker/TickerTest.java diff --git a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java index 0f10edbb0..7225616fa 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java @@ -34,18 +34,34 @@ public class LinuxHelper { private static final String LIBRARY_NAME = "c"; private static final VersionHelper UNKNOWN = new VersionHelper(0, 0, 0); private static final VersionHelper VERSION_2_6 = new VersionHelper(2, 6, 0); + private static final String STUB_PROPERTY = "chronicle.affinity.stub.linux"; + private static final boolean USE_STUB = Boolean.getBoolean(STUB_PROPERTY); + private static final BitSet STUB_AFFINITY = new BitSet(); + private static volatile int STUB_CPU = 0; + private static volatile int STUB_PID = 4242; + private static final ThreadLocal STUB_THREAD_ID = ThreadLocal.withInitial(() -> 3000); private static final VersionHelper version; + static { + if (USE_STUB && STUB_AFFINITY.isEmpty()) { + STUB_AFFINITY.set(0); + } + } + + private static final CLibrary LIBRARY = loadLibrary(); + static { final utsname uname = new utsname(); - VersionHelper ver = UNKNOWN; - try { - if (CLibrary.INSTANCE.uname(uname) == 0) { - ver = new VersionHelper(uname.getRealeaseVersion()); + VersionHelper ver = USE_STUB ? VERSION_2_6 : UNKNOWN; + if (!USE_STUB) { + try { + if (LIBRARY.uname(uname) == 0) { + ver = new VersionHelper(uname.getRealeaseVersion()); + } + } catch (Throwable e) { + LOGGER.debug("Failed to determine Linux version", e); } - } catch (Throwable e) { - LOGGER.debug("Failed to determine Linux version", e); } version = ver; @@ -54,7 +70,15 @@ public class LinuxHelper { public static @NotNull cpu_set_t sched_getaffinity() { - final CLibrary lib = CLibrary.INSTANCE; + if (USE_STUB) { + cpu_set_t cpuset = new cpu_set_t(); + long[] longs = STUB_AFFINITY.toLongArray(); + for (int i = 0; i < longs.length && i < cpuset.__bits.length; i++) { + cpuset.__bits[i].setValue(longs[i]); + } + return cpuset; + } + final CLibrary lib = LIBRARY; final cpu_set_t cpuset = new cpu_set_t(); final int size = version.isSameOrNewer(VERSION_2_6) ? cpu_set_t.SIZE_OF_CPU_SET_T : NativeLong.SIZE; @@ -75,7 +99,14 @@ public static void sched_setaffinity(final BitSet affinity) { } public static void sched_setaffinity(final int pid, final BitSet affinity) { - final CLibrary lib = CLibrary.INSTANCE; + if (USE_STUB) { + STUB_AFFINITY.clear(); + STUB_AFFINITY.or(affinity); + int next = affinity.nextSetBit(0); + STUB_CPU = next >= 0 ? next : 0; + return; + } + final CLibrary lib = LIBRARY; final cpu_set_t cpuset = new cpu_set_t(); final int size = version.isSameOrNewer(VERSION_2_6) ? cpu_set_t.SIZE_OF_CPU_SET_T : NativeLong.SIZE; final long[] bits = affinity.toLongArray(); @@ -99,7 +130,10 @@ public static void sched_setaffinity(final int pid, final BitSet affinity) { } public static int sched_getcpu() { - final CLibrary lib = CLibrary.INSTANCE; + if (USE_STUB) { + return STUB_CPU; + } + final CLibrary lib = LIBRARY; try { final int ret = lib.sched_getcpu(); if (ret < 0) { @@ -136,7 +170,10 @@ public static int sched_getcpu() { } public static int getpid() { - final CLibrary lib = CLibrary.INSTANCE; + if (USE_STUB) { + return STUB_PID; + } + final CLibrary lib = LIBRARY; try { final int ret = lib.getpid(); if (ret < 0) { @@ -149,7 +186,10 @@ public static int getpid() { } public static int syscall(int number, Object... args) { - final CLibrary lib = CLibrary.INSTANCE; + if (USE_STUB) { + return STUB_THREAD_ID.get(); + } + final CLibrary lib = LIBRARY; try { final int ret = lib.syscall(number, args); if (ret < 0) { @@ -162,8 +202,6 @@ public static int syscall(int number, Object... args) { } interface CLibrary extends Library { - CLibrary INSTANCE = Native.load(LIBRARY_NAME, CLibrary.class); - int sched_setaffinity(final int pid, final int cpusetsize, final cpu_set_t cpuset) throws LastErrorException; @@ -181,6 +219,34 @@ int sched_getaffinity(final int pid, int syscall(int number, Object... args) throws LastErrorException; } + private static CLibrary loadLibrary() { + if (USE_STUB) { + return null; + } + return Native.load(LIBRARY_NAME, CLibrary.class); + } + + static boolean usingStub() { + return USE_STUB; + } + + static void setStubCpu(int cpu) { + if (USE_STUB) { + STUB_CPU = cpu; + } + } + + static void setStubAffinity(BitSet affinity) { + if (USE_STUB) { + STUB_AFFINITY.clear(); + STUB_AFFINITY.or(affinity); + } + } + + static int stubThreadId() { + return STUB_THREAD_ID.get(); + } + /** * Structure describing the system and machine. */ diff --git a/affinity/src/main/java/net/openhft/affinity/impl/OSXJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/OSXJNAAffinity.java index 39db80875..fa8df71f6 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/OSXJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/OSXJNAAffinity.java @@ -36,6 +36,9 @@ public enum OSXJNAAffinity implements IAffinity { INSTANCE; private static final Logger LOGGER = LoggerFactory.getLogger(OSXJNAAffinity.class); private final ThreadLocal THREAD_ID = new ThreadLocal<>(); + private static final String STUB_PROPERTY = "chronicle.affinity.stub.osx"; + private static final boolean USE_STUB = Boolean.getBoolean(STUB_PROPERTY); + private static final CLibrary LIBRARY = loadLibrary(); @Override public BitSet getAffinity() { @@ -61,7 +64,7 @@ public int getProcessId() { public int getThreadId() { Integer tid = THREAD_ID.get(); if (tid == null) { - tid = CLibrary.INSTANCE.pthread_self(); + tid = LIBRARY.pthread_self(); //The tid assumed to be an unsigned 24 bit, see net.openhft.lang.Jvm.getMaxPid() tid = tid & 0xFFFFFF; THREAD_ID.set(tid); @@ -70,8 +73,13 @@ public int getThreadId() { } interface CLibrary extends Library { - CLibrary INSTANCE = Native.load("libpthread.dylib", CLibrary.class); - int pthread_self() throws LastErrorException; } + + private static CLibrary loadLibrary() { + if (USE_STUB) { + return () -> 0x123456; + } + return Native.load("libpthread.dylib", CLibrary.class); + } } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java index cfc2d6bc1..144da5a45 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java @@ -46,24 +46,42 @@ public enum PosixJNAAffinity implements IAffinity { private static final int PROCESS_ID; private static final int SYS_gettid = Utilities.is64Bit() ? 186 : 224; private static final Object[] NO_ARGS = {}; + private static final String STUB_PROPERTY = "chronicle.affinity.stub.posix"; + private static final boolean USE_STUB = Boolean.getBoolean(STUB_PROPERTY); + private static final BitSet STUB_AFFINITY = new BitSet(); + private static volatile int STUB_CPU = 0; + private static final ThreadLocal STUB_THREAD_ID = ThreadLocal.withInitial(() -> 2000); + private static final CLibrary LIBRARY = loadLibrary(); static { int processId; - try { - processId = CLibrary.INSTANCE.getpid(); - } catch (Exception ignored) { - processId = -1; + if (USE_STUB) { + processId = 1; + } else { + try { + processId = LIBRARY.getpid(); + } catch (Exception ignored) { + processId = -1; + } } PROCESS_ID = processId; } static { - boolean loaded = false; - try { - INSTANCE.getAffinity(); - loaded = true; - } catch (UnsatisfiedLinkError e) { - LOGGER.warn("Unable to load jna library", e); + if (USE_STUB && STUB_AFFINITY.isEmpty()) { + STUB_AFFINITY.set(0); + } + } + + static { + boolean loaded = USE_STUB; + if (!USE_STUB) { + try { + INSTANCE.getAffinity(); + loaded = true; + } catch (UnsatisfiedLinkError e) { + LOGGER.warn("Unable to load jna library", e); + } } LOADED = loaded; } @@ -72,7 +90,10 @@ public enum PosixJNAAffinity implements IAffinity { @Override public BitSet getAffinity() { - final CLibrary lib = CLibrary.INSTANCE; + if (USE_STUB) { + return (BitSet) STUB_AFFINITY.clone(); + } + final CLibrary lib = LIBRARY; final int procs = Runtime.getRuntime().availableProcessors(); final int cpuSetSizeInBytes = CpuSetUtil.requiredBytesForLogicalProcessors(procs); final Memory cpusetArray = new Memory(cpuSetSizeInBytes); @@ -109,12 +130,19 @@ public BitSet getAffinity() { @Override public void setAffinity(final BitSet affinity) { - int procs = Runtime.getRuntime().availableProcessors(); if (affinity.isEmpty()) { throw new IllegalArgumentException("Cannot set zero affinity"); } + if (USE_STUB) { + STUB_AFFINITY.clear(); + STUB_AFFINITY.or(affinity); + int nextSetBit = affinity.nextSetBit(0); + STUB_CPU = nextSetBit >= 0 ? nextSetBit : 0; + return; + } + int procs = Runtime.getRuntime().availableProcessors(); - final CLibrary lib = CLibrary.INSTANCE; + final CLibrary lib = LIBRARY; final int cpuSetSizeInBytes = CpuSetUtil.requiredBytesForMask(affinity, procs); byte[] buff = new byte[cpuSetSizeInBytes]; CpuSetUtil.writeMask(affinity, buff); @@ -148,7 +176,10 @@ public void setAffinity(final BitSet affinity) { @Override public int getCpu() { - final CLibrary lib = CLibrary.INSTANCE; + if (USE_STUB) { + return STUB_CPU; + } + final CLibrary lib = LIBRARY; try { final int ret = lib.sched_getcpu(); if (ret < 0) @@ -179,10 +210,13 @@ public int getProcessId() { @Override public int getThreadId() { + if (USE_STUB) { + return STUB_THREAD_ID.get(); + } if (Utilities.ISLINUX) { Integer tid = THREAD_ID.get(); if (tid == null) - THREAD_ID.set(tid = CLibrary.INSTANCE.syscall(SYS_gettid, NO_ARGS)); + THREAD_ID.set(tid = LIBRARY.syscall(SYS_gettid, NO_ARGS)); return tid; } return -1; @@ -192,8 +226,6 @@ public int getThreadId() { * @author BegemoT */ interface CLibrary extends Library { - CLibrary INSTANCE = Native.load(LIBRARY_NAME, CLibrary.class); - int sched_setaffinity(final int pid, final int cpusetsize, final PointerType cpuset) throws LastErrorException; @@ -212,4 +244,46 @@ int getcpu(final IntByReference cpu, int syscall(int number, Object... args) throws LastErrorException; } + + private static CLibrary loadLibrary() { + if (USE_STUB) { + return new StubPosixCLibrary(); + } + return Native.load(LIBRARY_NAME, CLibrary.class); + } + + private static final class StubPosixCLibrary implements CLibrary { + @Override + public int sched_setaffinity(int pid, int cpusetsize, PointerType cpuset) { + return 0; + } + + @Override + public int sched_getaffinity(int pid, int cpusetsize, PointerType cpuset) { + return 0; + } + + @Override + public int sched_getcpu() { + return STUB_CPU; + } + + @Override + public int getcpu(IntByReference cpu, IntByReference node, PointerType tcache) { + if (cpu != null) { + cpu.setValue(STUB_CPU); + } + return 0; + } + + @Override + public int getpid() { + return 1; + } + + @Override + public int syscall(int number, Object... args) { + return STUB_THREAD_ID.get(); + } + } } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/SolarisJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/SolarisJNAAffinity.java index ebe49b154..442aed61d 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/SolarisJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/SolarisJNAAffinity.java @@ -36,6 +36,9 @@ public enum SolarisJNAAffinity implements IAffinity { INSTANCE; private static final Logger LOGGER = LoggerFactory.getLogger(SolarisJNAAffinity.class); private final ThreadLocal THREAD_ID = new ThreadLocal<>(); + private static final String STUB_PROPERTY = "chronicle.affinity.stub.solaris"; + private static final boolean USE_STUB = Boolean.getBoolean(STUB_PROPERTY); + private static final CLibrary LIBRARY = loadLibrary(); @Override public BitSet getAffinity() { @@ -61,7 +64,7 @@ public int getProcessId() { public int getThreadId() { Integer tid = THREAD_ID.get(); if (tid == null) { - tid = CLibrary.INSTANCE.pthread_self(); + tid = LIBRARY.pthread_self(); //The tid assumed to be an unsigned 24 bit, see net.openhft.lang.Jvm.getMaxPid() tid = tid & 0xFFFFFF; THREAD_ID.set(tid); @@ -70,8 +73,13 @@ public int getThreadId() { } interface CLibrary extends Library { - CLibrary INSTANCE = Native.load("c", CLibrary.class); - int pthread_self() throws LastErrorException; } + + private static CLibrary loadLibrary() { + if (USE_STUB) { + return () -> 0x654321; + } + return Native.load("c", CLibrary.class); + } } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java index c64bd15be..608af267d 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -23,6 +23,7 @@ import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.LongByReference; import net.openhft.affinity.IAffinity; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,14 +43,46 @@ public enum WindowsJNAAffinity implements IAffinity { public static final boolean LOADED; private static final Logger LOGGER = LoggerFactory.getLogger(WindowsJNAAffinity.class); private static final ThreadLocal currentAffinity = new ThreadLocal<>(); + private static final String STUB_PROPERTY = "chronicle.affinity.stub.windows"; + private static final boolean USE_STUB = Boolean.getBoolean(STUB_PROPERTY); + + private static final class LibraryContainer { + final CLibrary library; + final boolean stub; + + LibraryContainer() { + if (USE_STUB) { + library = new StubWindowsCLibrary(); + stub = true; + return; + } + CLibrary lib; + boolean isStub = false; + try { + lib = Native.load("kernel32", CLibrary.class); + } catch (UnsatisfiedLinkError e) { + LOGGER.warn("Unable to load jna library", e); + lib = new StubWindowsCLibrary(); + isStub = true; + } + library = lib; + stub = isStub; + } + } + + private static final LibraryContainer LIBRARY_CONTAINER = new LibraryContainer(); + private static final CLibrary LIBRARY = LIBRARY_CONTAINER.library; + private static final boolean LIBRARY_IS_STUB = LIBRARY_CONTAINER.stub; static { boolean loaded = false; - try { - INSTANCE.getAffinity(); - loaded = true; - } catch (UnsatisfiedLinkError e) { - LOGGER.warn("Unable to load jna library", e); + if (!LIBRARY_IS_STUB) { + try { + INSTANCE.getAffinity(); + loaded = true; + } catch (UnsatisfiedLinkError e) { + LOGGER.warn("Unable to load jna library", e); + } } LOADED = loaded; } @@ -67,7 +100,7 @@ public BitSet getAffinity() { @Override public void setAffinity(final BitSet affinity) { - final CLibrary lib = CLibrary.INSTANCE; + final CLibrary lib = LIBRARY; WinDef.DWORD aff; long[] longs = affinity.toLongArray(); @@ -98,7 +131,7 @@ public void setAffinity(final BitSet affinity) { @Nullable private BitSet getAffinity0() { - final CLibrary lib = CLibrary.INSTANCE; + final CLibrary lib = LIBRARY; final LongByReference cpuset1 = new LongByReference(0); final LongByReference cpuset2 = new LongByReference(0); try { @@ -124,7 +157,7 @@ private WinNT.HANDLE handle(int pid) { } public int getTid() { - final CLibrary lib = CLibrary.INSTANCE; + final CLibrary lib = LIBRARY; try { return lib.GetCurrentThread(); @@ -140,14 +173,30 @@ public int getCpu() { @Override public int getProcessId() { - return Kernel32.INSTANCE.GetCurrentProcessId(); + if (LIBRARY_IS_STUB) { + return 1; + } + try { + return Kernel32.INSTANCE.GetCurrentProcessId(); + } catch (UnsatisfiedLinkError | NoClassDefFoundError e) { + return 1; + } } @Override public int getThreadId() { + if (LIBRARY_IS_STUB) { + return 1; + } Integer tid = THREAD_ID.get(); - if (tid == null) - THREAD_ID.set(tid = Kernel32.INSTANCE.GetCurrentThreadId()); + if (tid == null) { + try { + tid = Kernel32.INSTANCE.GetCurrentThreadId(); + } catch (UnsatisfiedLinkError | NoClassDefFoundError e) { + tid = 1; + } + THREAD_ID.set(tid); + } return tid; } @@ -155,12 +204,36 @@ public int getThreadId() { * @author BegemoT */ private interface CLibrary extends Library { - CLibrary INSTANCE = Native.load("kernel32", CLibrary.class); - int GetProcessAffinityMask(final WinNT.HANDLE pid, final PointerType lpProcessAffinityMask, final PointerType lpSystemAffinityMask) throws LastErrorException; void SetThreadAffinityMask(final WinNT.HANDLE pid, final WinDef.DWORD lpProcessAffinityMask) throws LastErrorException; int GetCurrentThread() throws LastErrorException; } + + @SuppressFBWarnings(value = "NM_METHOD_NAMING_CONVENTION", justification = "Method names must mirror WinAPI signatures for JNA compatibility") + private static final class StubWindowsCLibrary implements CLibrary { + private long mask = 1L; + + @Override + public int GetProcessAffinityMask(WinNT.HANDLE pid, PointerType lpProcessAffinityMask, PointerType lpSystemAffinityMask) { + if (lpProcessAffinityMask != null && lpProcessAffinityMask.getPointer() != null) { + lpProcessAffinityMask.getPointer().setLong(0, mask); + } + if (lpSystemAffinityMask != null && lpSystemAffinityMask.getPointer() != null) { + lpSystemAffinityMask.getPointer().setLong(0, mask); + } + return 1; + } + + @Override + public void SetThreadAffinityMask(WinNT.HANDLE pid, WinDef.DWORD lpProcessAffinityMask) { + mask = lpProcessAffinityMask.longValue(); + } + + @Override + public int GetCurrentThread() { + return 1; + } + } } diff --git a/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java b/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java new file mode 100644 index 000000000..593fa4c02 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class MicroJitterSamplerTest { + + private static final Field COUNT_FIELD; + private static final Field TOTAL_TIME_FIELD; + private static final Method AS_STRING_METHOD; + + static { + try { + COUNT_FIELD = MicroJitterSampler.class.getDeclaredField("count"); + COUNT_FIELD.setAccessible(true); + TOTAL_TIME_FIELD = MicroJitterSampler.class.getDeclaredField("totalTime"); + TOTAL_TIME_FIELD.setAccessible(true); + AS_STRING_METHOD = MicroJitterSampler.class.getDeclaredMethod("asString", long.class); + AS_STRING_METHOD.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Failed to access MicroJitterSampler internals for testing", e); + } + } + + @Test + public void resetClearsCountsAndTotalTime() throws Exception { + MicroJitterSampler sampler = new MicroJitterSampler(); + int[] counts = (int[]) COUNT_FIELD.get(sampler); + Arrays.fill(counts, 7); + TOTAL_TIME_FIELD.setLong(sampler, 42L); + + sampler.reset(); + + assertArrayEquals("All jitter buckets should reset to zero after reset", + new int[counts.length], counts); + assertEquals("Total time should reset to zero", 0L, TOTAL_TIME_FIELD.getLong(sampler)); + } + + @Test + public void sampleAccumulatesTotalTime() throws Exception { + MicroJitterSampler sampler = new MicroJitterSampler(); + sampler.reset(); + + sampler.sample(1_000L); + sampler.sample(500L); + + assertEquals("Total sampled interval should accumulate nanos", + 1_500L, TOTAL_TIME_FIELD.getLong(sampler)); + } + + @Test + public void printFormatsCountsPerHour() throws Exception { + MicroJitterSampler sampler = new MicroJitterSampler(); + int[] counts = (int[]) COUNT_FIELD.get(sampler); + Arrays.fill(counts, 0); + counts[0] = 2; // 2us bucket + counts[10] = 1; // 60us bucket + TOTAL_TIME_FIELD.setLong(sampler, 3_600_000_000_000L); // 1 hour in ns + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (PrintStream ps = new PrintStream(baos, true, StandardCharsets.UTF_8)) { + sampler.print(ps); + } + + String output = new String(baos.toByteArray(), StandardCharsets.UTF_8); + String lineSeparator = System.lineSeparator(); + String expected = "After 3600 seconds, the average per hour was" + lineSeparator + + "2us\t2" + lineSeparator + + "60us\t1" + lineSeparator + lineSeparator; + assertEquals(expected, output); + } + + @Test + public void asStringConvertsUnits() throws Exception { + assertEquals("999ns", AS_STRING_METHOD.invoke(null, 999L)); + assertEquals("2us", AS_STRING_METHOD.invoke(null, 2_000L)); + assertEquals("2ms", AS_STRING_METHOD.invoke(null, 2_000_000L)); + assertEquals("3sec", AS_STRING_METHOD.invoke(null, 3_000_000_000L)); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java new file mode 100644 index 000000000..29a05368a --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class LinuxJNAAffinityStubTest { + + @BeforeClass + public static void enableStub() { + System.setProperty("chronicle.affinity.stub.linux", "true"); + } + + @Test + public void stubbedHelperProvidesDeterministicValues() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + Assume.assumeFalse("Skip stub when running on native Linux", osName.contains("linux")); + + BitSet initial = new BitSet(); + initial.set(2); + LinuxHelper.setStubAffinity(initial); + LinuxHelper.setStubCpu(2); + + BitSet observed = LinuxJNAAffinity.INSTANCE.getAffinity(); + assertTrue("Affinity should reflect stub mask", observed.get(2)); + assertTrue("Linux affinity stub reports loaded", LinuxJNAAffinity.LOADED); + assertTrue("Process id should be positive", LinuxJNAAffinity.INSTANCE.getProcessId() > 0); + + BitSet update = new BitSet(); + update.set(5); + LinuxJNAAffinity.INSTANCE.setAffinity(update); + + BitSet updated = LinuxJNAAffinity.INSTANCE.getAffinity(); + assertTrue("Updated affinity should reflect new cpu", updated.get(5)); + assertEquals("Stub CPU should follow latest assignment", 5, LinuxJNAAffinity.INSTANCE.getCpu()); + + int tid = LinuxJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Thread id should be stable for stubbed helper", tid, LinuxJNAAffinity.INSTANCE.getThreadId()); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java new file mode 100644 index 000000000..6fa532da5 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class OSXJNAAffinityStubTest { + + @BeforeClass + public static void enableStub() { + System.setProperty("chronicle.affinity.stub.osx", "true"); + } + + @Test + public void stubReturnsMaskedThreadId() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + Assume.assumeFalse("Stub should not override native macOS library", osName.contains("mac")); + + BitSet affinity = OSXJNAAffinity.INSTANCE.getAffinity(); + assertFalse("Affinity should be empty on macOS stub", affinity.get(0)); + + int tid = OSXJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Stubbed pthread id should match configured constant", 0x123456, tid); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java new file mode 100644 index 000000000..7eadc4c3e --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PosixJNAAffinityStubTest { + + @BeforeClass + public static void enableStub() { + System.setProperty("chronicle.affinity.stub.posix", "true"); + } + + @Test + public void stubTracksAffinityAndCpu() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + Assume.assumeFalse("Use stub only when native POSIX calls are unavailable", osName.contains("linux")); + + BitSet mask = new BitSet(); + mask.set(1); + mask.set(4); + PosixJNAAffinity.INSTANCE.setAffinity(mask); + + BitSet actual = PosixJNAAffinity.INSTANCE.getAffinity(); + assertEquals("Stub should echo assigned affinity mask", mask, actual); + assertEquals("Stub CPU should follow lowest set bit", 1, PosixJNAAffinity.INSTANCE.getCpu()); + assertTrue("Process id should be positive", PosixJNAAffinity.INSTANCE.getProcessId() > 0); + assertTrue("Stub reports loaded state", PosixJNAAffinity.LOADED); + + int tid = PosixJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Thread id should be stable across calls", tid, PosixJNAAffinity.INSTANCE.getThreadId()); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java new file mode 100644 index 000000000..36006723f --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SolarisJNAAffinityStubTest { + + @BeforeClass + public static void enableStub() { + System.setProperty("chronicle.affinity.stub.solaris", "true"); + } + + @Test + public void stubReturnsConsistentThreadId() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + Assume.assumeFalse("Do not override native Solaris library", osName.contains("sunos")); + + assertTrue("Affinity remains empty for Solaris stub", SolarisJNAAffinity.INSTANCE.getAffinity().isEmpty()); + + int tid = SolarisJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Stubbed pthread id should match configured constant", 0x654321, tid); + assertEquals("Thread id should be cached per thread", tid, SolarisJNAAffinity.INSTANCE.getThreadId()); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java new file mode 100644 index 000000000..930e60818 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class WindowsJNAAffinityStubTest { + + @BeforeClass + public static void enableStub() { + System.setProperty("chronicle.affinity.stub.windows", "true"); + } + + @Test + public void stubProvidesDeterministicAffinity() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + Assume.assumeFalse("Stub test should not run on native Windows", osName.contains("win")); + + BitSet mask = new BitSet(); + mask.set(3); + WindowsJNAAffinity.INSTANCE.setAffinity(mask); + + BitSet actual = WindowsJNAAffinity.INSTANCE.getAffinity(); + assertTrue("Stub should reflect affinity mask", actual.get(3)); + assertTrue("Process id should be positive", WindowsJNAAffinity.INSTANCE.getProcessId() > 0); + assertFalse("Stubbed implementation should report as not loaded", WindowsJNAAffinity.LOADED); + + int threadId = WindowsJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Thread id should be stable for same thread", threadId, WindowsJNAAffinity.INSTANCE.getThreadId()); + + } +} diff --git a/affinity/src/test/java/net/openhft/ticker/TickerTest.java b/affinity/src/test/java/net/openhft/ticker/TickerTest.java new file mode 100644 index 000000000..b4ea963e0 --- /dev/null +++ b/affinity/src/test/java/net/openhft/ticker/TickerTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.ticker; + +import net.openhft.ticker.impl.JNIClock; +import net.openhft.ticker.impl.SystemClock; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class TickerTest { + + @Test + public void instanceMatchesLoadedClock() { + if (JNIClock.LOADED) { + assertSame("When JNI clock is available it should back Ticker.INSTANCE", + JNIClock.INSTANCE, Ticker.INSTANCE); + } else { + assertSame("Without JNI support the system clock should be used", + SystemClock.INSTANCE, Ticker.INSTANCE); + } + } + + @Test + public void conversionsUseUnderlyingClock() { + long ticks = Ticker.ticks(); + long nanos = Ticker.toNanos(ticks); + double micros = Ticker.toMicros(ticks); + + assertTrue("Ticker ticks should never be negative", ticks >= 0); + assertTrue("Ticker nanos should never be negative", nanos >= 0); + assertTrue("Ticker micros should never be negative", micros >= 0.0); + + long reference = 123_456_000L; + double expectedMicros = reference / 1_000.0; + + if (Ticker.INSTANCE == SystemClock.INSTANCE) { + assertEquals("System clock should treat ticks as nanos", reference, Ticker.toNanos(reference)); + assertEquals("System clock converts nanos to micros using division", + expectedMicros, Ticker.toMicros(reference), 0.0001); + } else { + long converted = Ticker.toNanos(reference); + assertEquals("Native clock should convert ticks back to nanos consistently", + converted, Ticker.toNanos(reference)); + assertEquals("Native clock micros conversion should align with nanos conversion", + converted / 1_000.0, Ticker.toMicros(reference), converted * 0.01); + } + + long later = Ticker.nanoTime(); + assertTrue("nanoTime should advance monotonically", later >= nanos); + } +} From 19e804cd9f8b18a210cfbfbb5dc6a70447b59374 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 10:01:21 +0000 Subject: [PATCH 09/20] Restore real affinity runs by scoping stub flags --- .../impl/LinuxJNAAffinityStubTest.java | 49 ++++++++++--------- .../affinity/impl/OSXJNAAffinityStubTest.java | 27 +++++----- .../impl/PosixJNAAffinityStubTest.java | 43 +++++++++------- .../impl/SolarisJNAAffinityStubTest.java | 27 +++++----- .../impl/WindowsJNAAffinityStubTest.java | 40 ++++++++------- 5 files changed, 105 insertions(+), 81 deletions(-) diff --git a/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java index 29a05368a..22051dbd2 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java @@ -17,7 +17,6 @@ package net.openhft.affinity.impl; import org.junit.Assume; -import org.junit.BeforeClass; import org.junit.Test; import java.util.BitSet; @@ -28,35 +27,41 @@ public class LinuxJNAAffinityStubTest { - @BeforeClass - public static void enableStub() { - System.setProperty("chronicle.affinity.stub.linux", "true"); - } - @Test public void stubbedHelperProvidesDeterministicValues() { String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); Assume.assumeFalse("Skip stub when running on native Linux", osName.contains("linux")); - BitSet initial = new BitSet(); - initial.set(2); - LinuxHelper.setStubAffinity(initial); - LinuxHelper.setStubCpu(2); + String previous = System.getProperty("chronicle.affinity.stub.linux"); + try { + System.setProperty("chronicle.affinity.stub.linux", "true"); + + BitSet initial = new BitSet(); + initial.set(2); + LinuxHelper.setStubAffinity(initial); + LinuxHelper.setStubCpu(2); - BitSet observed = LinuxJNAAffinity.INSTANCE.getAffinity(); - assertTrue("Affinity should reflect stub mask", observed.get(2)); - assertTrue("Linux affinity stub reports loaded", LinuxJNAAffinity.LOADED); - assertTrue("Process id should be positive", LinuxJNAAffinity.INSTANCE.getProcessId() > 0); + BitSet observed = LinuxJNAAffinity.INSTANCE.getAffinity(); + assertTrue("Affinity should reflect stub mask", observed.get(2)); + assertTrue("Linux affinity stub reports loaded", LinuxJNAAffinity.LOADED); + assertTrue("Process id should be positive", LinuxJNAAffinity.INSTANCE.getProcessId() > 0); - BitSet update = new BitSet(); - update.set(5); - LinuxJNAAffinity.INSTANCE.setAffinity(update); + BitSet update = new BitSet(); + update.set(5); + LinuxJNAAffinity.INSTANCE.setAffinity(update); - BitSet updated = LinuxJNAAffinity.INSTANCE.getAffinity(); - assertTrue("Updated affinity should reflect new cpu", updated.get(5)); - assertEquals("Stub CPU should follow latest assignment", 5, LinuxJNAAffinity.INSTANCE.getCpu()); + BitSet updated = LinuxJNAAffinity.INSTANCE.getAffinity(); + assertTrue("Updated affinity should reflect new cpu", updated.get(5)); + assertEquals("Stub CPU should follow latest assignment", 5, LinuxJNAAffinity.INSTANCE.getCpu()); - int tid = LinuxJNAAffinity.INSTANCE.getThreadId(); - assertEquals("Thread id should be stable for stubbed helper", tid, LinuxJNAAffinity.INSTANCE.getThreadId()); + int tid = LinuxJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Thread id should be stable for stubbed helper", tid, LinuxJNAAffinity.INSTANCE.getThreadId()); + } finally { + if (previous == null) { + System.clearProperty("chronicle.affinity.stub.linux"); + } else { + System.setProperty("chronicle.affinity.stub.linux", previous); + } + } } } diff --git a/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java index 6fa532da5..5a04ef50a 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java @@ -17,7 +17,6 @@ package net.openhft.affinity.impl; import org.junit.Assume; -import org.junit.BeforeClass; import org.junit.Test; import java.util.BitSet; @@ -28,20 +27,26 @@ public class OSXJNAAffinityStubTest { - @BeforeClass - public static void enableStub() { - System.setProperty("chronicle.affinity.stub.osx", "true"); - } - @Test public void stubReturnsMaskedThreadId() { String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); Assume.assumeFalse("Stub should not override native macOS library", osName.contains("mac")); - BitSet affinity = OSXJNAAffinity.INSTANCE.getAffinity(); - assertFalse("Affinity should be empty on macOS stub", affinity.get(0)); - - int tid = OSXJNAAffinity.INSTANCE.getThreadId(); - assertEquals("Stubbed pthread id should match configured constant", 0x123456, tid); + String previous = System.getProperty("chronicle.affinity.stub.osx"); + try { + System.setProperty("chronicle.affinity.stub.osx", "true"); + + BitSet affinity = OSXJNAAffinity.INSTANCE.getAffinity(); + assertFalse("Affinity should be empty on macOS stub", affinity.get(0)); + + int tid = OSXJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Stubbed pthread id should match configured constant", 0x123456, tid); + } finally { + if (previous == null) { + System.clearProperty("chronicle.affinity.stub.osx"); + } else { + System.setProperty("chronicle.affinity.stub.osx", previous); + } + } } } diff --git a/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java index 7eadc4c3e..c52494d9b 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java @@ -17,7 +17,6 @@ package net.openhft.affinity.impl; import org.junit.Assume; -import org.junit.BeforeClass; import org.junit.Test; import java.util.BitSet; @@ -28,28 +27,34 @@ public class PosixJNAAffinityStubTest { - @BeforeClass - public static void enableStub() { - System.setProperty("chronicle.affinity.stub.posix", "true"); - } - @Test public void stubTracksAffinityAndCpu() { String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); Assume.assumeFalse("Use stub only when native POSIX calls are unavailable", osName.contains("linux")); - BitSet mask = new BitSet(); - mask.set(1); - mask.set(4); - PosixJNAAffinity.INSTANCE.setAffinity(mask); - - BitSet actual = PosixJNAAffinity.INSTANCE.getAffinity(); - assertEquals("Stub should echo assigned affinity mask", mask, actual); - assertEquals("Stub CPU should follow lowest set bit", 1, PosixJNAAffinity.INSTANCE.getCpu()); - assertTrue("Process id should be positive", PosixJNAAffinity.INSTANCE.getProcessId() > 0); - assertTrue("Stub reports loaded state", PosixJNAAffinity.LOADED); - - int tid = PosixJNAAffinity.INSTANCE.getThreadId(); - assertEquals("Thread id should be stable across calls", tid, PosixJNAAffinity.INSTANCE.getThreadId()); + String previous = System.getProperty("chronicle.affinity.stub.posix"); + try { + System.setProperty("chronicle.affinity.stub.posix", "true"); + + BitSet mask = new BitSet(); + mask.set(1); + mask.set(4); + PosixJNAAffinity.INSTANCE.setAffinity(mask); + + BitSet actual = PosixJNAAffinity.INSTANCE.getAffinity(); + assertEquals("Stub should echo assigned affinity mask", mask, actual); + assertEquals("Stub CPU should follow lowest set bit", 1, PosixJNAAffinity.INSTANCE.getCpu()); + assertTrue("Process id should be positive", PosixJNAAffinity.INSTANCE.getProcessId() > 0); + assertTrue("Stub reports loaded state", PosixJNAAffinity.LOADED); + + int tid = PosixJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Thread id should be stable across calls", tid, PosixJNAAffinity.INSTANCE.getThreadId()); + } finally { + if (previous == null) { + System.clearProperty("chronicle.affinity.stub.posix"); + } else { + System.setProperty("chronicle.affinity.stub.posix", previous); + } + } } } diff --git a/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java index 36006723f..3fed2ebe2 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java @@ -17,7 +17,6 @@ package net.openhft.affinity.impl; import org.junit.Assume; -import org.junit.BeforeClass; import org.junit.Test; import java.util.Locale; @@ -27,20 +26,26 @@ public class SolarisJNAAffinityStubTest { - @BeforeClass - public static void enableStub() { - System.setProperty("chronicle.affinity.stub.solaris", "true"); - } - @Test public void stubReturnsConsistentThreadId() { String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); Assume.assumeFalse("Do not override native Solaris library", osName.contains("sunos")); - assertTrue("Affinity remains empty for Solaris stub", SolarisJNAAffinity.INSTANCE.getAffinity().isEmpty()); - - int tid = SolarisJNAAffinity.INSTANCE.getThreadId(); - assertEquals("Stubbed pthread id should match configured constant", 0x654321, tid); - assertEquals("Thread id should be cached per thread", tid, SolarisJNAAffinity.INSTANCE.getThreadId()); + String previous = System.getProperty("chronicle.affinity.stub.solaris"); + try { + System.setProperty("chronicle.affinity.stub.solaris", "true"); + + assertTrue("Affinity remains empty for Solaris stub", SolarisJNAAffinity.INSTANCE.getAffinity().isEmpty()); + + int tid = SolarisJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Stubbed pthread id should match configured constant", 0x654321, tid); + assertEquals("Thread id should be cached per thread", tid, SolarisJNAAffinity.INSTANCE.getThreadId()); + } finally { + if (previous == null) { + System.clearProperty("chronicle.affinity.stub.solaris"); + } else { + System.setProperty("chronicle.affinity.stub.solaris", previous); + } + } } } diff --git a/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java index 930e60818..ad7eaaa10 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java @@ -17,7 +17,6 @@ package net.openhft.affinity.impl; import org.junit.Assume; -import org.junit.BeforeClass; import org.junit.Test; import java.util.BitSet; @@ -29,27 +28,32 @@ public class WindowsJNAAffinityStubTest { - @BeforeClass - public static void enableStub() { - System.setProperty("chronicle.affinity.stub.windows", "true"); - } - @Test public void stubProvidesDeterministicAffinity() { String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); Assume.assumeFalse("Stub test should not run on native Windows", osName.contains("win")); - BitSet mask = new BitSet(); - mask.set(3); - WindowsJNAAffinity.INSTANCE.setAffinity(mask); - - BitSet actual = WindowsJNAAffinity.INSTANCE.getAffinity(); - assertTrue("Stub should reflect affinity mask", actual.get(3)); - assertTrue("Process id should be positive", WindowsJNAAffinity.INSTANCE.getProcessId() > 0); - assertFalse("Stubbed implementation should report as not loaded", WindowsJNAAffinity.LOADED); - - int threadId = WindowsJNAAffinity.INSTANCE.getThreadId(); - assertEquals("Thread id should be stable for same thread", threadId, WindowsJNAAffinity.INSTANCE.getThreadId()); - + String previous = System.getProperty("chronicle.affinity.stub.windows"); + try { + System.setProperty("chronicle.affinity.stub.windows", "true"); + + BitSet mask = new BitSet(); + mask.set(3); + WindowsJNAAffinity.INSTANCE.setAffinity(mask); + + BitSet actual = WindowsJNAAffinity.INSTANCE.getAffinity(); + assertTrue("Stub should reflect affinity mask", actual.get(3)); + assertTrue("Process id should be positive", WindowsJNAAffinity.INSTANCE.getProcessId() > 0); + assertFalse("Stubbed implementation should report as not loaded", WindowsJNAAffinity.LOADED); + + int threadId = WindowsJNAAffinity.INSTANCE.getThreadId(); + assertEquals("Thread id should be stable for same thread", threadId, WindowsJNAAffinity.INSTANCE.getThreadId()); + } finally { + if (previous == null) { + System.clearProperty("chronicle.affinity.stub.windows"); + } else { + System.setProperty("chronicle.affinity.stub.windows", previous); + } + } } } From 6bfa3ea509c295043a86b86b4b634c019f509786 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 10:21:38 +0000 Subject: [PATCH 10/20] Refactor code comments and formatting for clarity and consistency --- LICENSE.adoc | 10 +- README.adoc | 9 + affinity-test/pom.xml | 13 +- affinity/pom.xml | 8 +- affinity/src/main/config/spotbugs-exclude.xml | 316 ++++++++++-------- .../affinity/impl/WindowsJNAAffinity.java | 2 +- .../lockchecker/FileLockBasedLockChecker.java | 2 +- .../impl/WindowsJNAAffinityStubTest.java | 4 +- pom.xml | 5 +- 9 files changed, 200 insertions(+), 169 deletions(-) diff --git a/LICENSE.adoc b/LICENSE.adoc index f93a31eb3..f45056642 100644 --- a/LICENSE.adoc +++ b/LICENSE.adoc @@ -1,13 +1,9 @@ == Copyright 2016-2025 chronicle.software -Licensed under the *Apache License, Version 2.0* (the "License"); -you may not use this file except in compliance with the License. +Licensed under the *Apache License, Version 2.0* (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and limitations under the License. diff --git a/README.adoc b/README.adoc index d8ae82fb9..06e3fd2da 100644 --- a/README.adoc +++ b/README.adoc @@ -8,9 +8,11 @@ image::docs/images/Thread-Affinity_line.png[width=20%] [#image-maven] [caption="",link=https://maven-badges.herokuapp.com/maven-central/net.openhft/affinity] image::https://maven-badges.herokuapp.com/maven-central/net.openhft/affinity/badge.svg[] + image:https://javadoc.io/badge2/net.openhft/affinity/javadoc.svg[link="https://www.javadoc.io/doc/net.openhft/affinity/latest/index.html"] == Overview + Lets you bind a thread to a given core, this can improve performance (this library works best on linux). OpenHFT Java Thread Affinity library @@ -63,6 +65,7 @@ To work around this problem, fork the repository, and override the `` t Or download jna.jar and jna-platform.jar from the JNA project and add them to your classpath. === How does CPU allocation work? + The library will read your `/proc/cpuinfo` if you have one or provide one and it will determine your CPU layout. If you don't have one it will assume every CPU is on one CPU socket. @@ -123,6 +126,7 @@ sudo reboot == Using AffinityLock === Acquiring a CPU lock for a thread + You can acquire a lock for a CPU in the following way: .In Java 6 @@ -147,6 +151,7 @@ try (AffinityLock al = AffinityLock.acquireLock()) { You have further options such as === Acquiring a CORE lock for a thread + You can reserve a whole core. If you have hyper-threading enabled, this will use one CPU and leave it's twin CPU unused. @@ -181,6 +186,7 @@ try (final AffinityLock al = AffinityLock.acquireLock()) { In this example, the library will prefer a free CPU on the same Socket as the first thread, otherwise it will pick any free CPU. === Affinity strategies + The `AffinityStrategies` enum defines hints for selecting a CPU relative to an existing lock. [options="header",cols="1,3"] @@ -195,6 +201,7 @@ The `AffinityStrategies` enum defines hints for selecting a CPU relative to an e |=== === Getting the thread id + You can get the current thread id using [source,java] @@ -203,6 +210,7 @@ int threadId = AffinitySupport.getThreadId(); ---- === Determining which CPU you are running on + You can get the current CPU being used by [source,java] @@ -321,6 +329,7 @@ For an article on how much difference affinity can make and how to use it http:/ == Questions and Answers === Question: How to lock a specific cpuId + I am currently working on a project related to deadlock detection in multithreaded programs in java. We are trying to run threads on different processors and thus came across your github posts regarding the same. https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started Being a beginner, I have little knowledge and thus need your assistance. diff --git a/affinity-test/pom.xml b/affinity-test/pom.xml index 7b3901533..450f04bd7 100644 --- a/affinity-test/pom.xml +++ b/affinity-test/pom.xml @@ -15,14 +15,15 @@ ~ limitations under the License. --> - + 4.0.0 net.openhft java-parent-pom 1.27ea1 - + affinity-test @@ -353,9 +354,9 @@ scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git - ea - scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git - scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git - + ea + scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git + scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git + diff --git a/affinity/pom.xml b/affinity/pom.xml index f78d5f8e4..8772bdbab 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -15,14 +15,15 @@ ~ limitations under the License. --> - + 4.0.0 net.openhft java-parent-pom 1.27ea1 - + affinity @@ -280,7 +281,8 @@ true true - ${project.basedir}/src/main/config/pmd-exclude.properties + ${project.basedir}/src/main/config/pmd-exclude.properties + diff --git a/affinity/src/main/config/spotbugs-exclude.xml b/affinity/src/main/config/spotbugs-exclude.xml index a8ba0d86d..0c3946b4f 100644 --- a/affinity/src/main/config/spotbugs-exclude.xml +++ b/affinity/src/main/config/spotbugs-exclude.xml @@ -1,149 +1,173 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AFF-SEC-205: Only JVM-local diagnostics (stack traces, thread names) reach this logger; no remote input accepted. Structured logging follow-up captured under AFF-SEC-310. - - - - - - AFF-SEC-206: Lock ownership details are derived from in-process state; newline sanitisation is tracked separately (AFF-SEC-312). - - - - - - AFF-SEC-207: Boot class path warnings expose deterministic JVM configuration strings; acceptable during CLI diagnostics. - - - - - - AFF-SEC-208: Reports only Chronicle-maintained lock metadata; no external input path. - - - - - - AFF-SEC-209: Inventory messages derive from local lock files owned by operators; newline injection not possible. - - - - - - AFF-SEC-210: Static warning surfaced when no affinity backend is present; message is constant. - - - - - - AFF-SEC-211: OSX diagnostics emit static capability text; retained for operator visibility. - - - - - - AFF-SEC-212: Solaris fallback logging exposes only fixed strings about missing APIs. - - - - - - AFF-OPS-118: Reads CPU topology from /proc and /sys under operator control; inputs originate from the kernel and are trusted for tooling. - - - - - - AFF-SEC-213: Logs static strings about DLL discovery; no newline injection vector. - - - - - - AFF-OPS-204: Lock checker must grant cooperative processes access to shared files; permissions and log output are limited to Chronicle-managed directories until AFF-OPS-330 revisits the design. - - - - - - AFF-SEC-214: Hardware timer diagnostics are emitted to trusted operator logs; no user-controlled data flows in. - - - - - - AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). - - - - - - AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). - - - - - - AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). - - - - - - AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled (AFF-SEC-310, AFF-OPS-330). - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AFF-SEC-205: Only JVM-local diagnostics (stack traces, thread names) reach this logger; no remote + input accepted. Structured logging follow-up captured under AFF-SEC-310. + + + + + + + AFF-SEC-206: Lock ownership details are derived from in-process state; newline sanitisation is + tracked separately (AFF-SEC-312). + + + + + + + AFF-SEC-207: Boot class path warnings expose deterministic JVM configuration strings; acceptable + during CLI diagnostics. + + + + + + + AFF-SEC-208: Reports only Chronicle-maintained lock metadata; no external input path. + + + + + + AFF-SEC-209: Inventory messages derive from local lock files owned by operators; newline injection + not possible. + + + + + + + AFF-SEC-210: Static warning surfaced when no affinity backend is present; message is constant. + + + + + + + AFF-SEC-211: OSX diagnostics emit static capability text; retained for operator visibility. + + + + + + + AFF-SEC-212: Solaris fallback logging exposes only fixed strings about missing APIs. + + + + + + AFF-OPS-118: Reads CPU topology from /proc and /sys under operator control; inputs originate from + the kernel and are trusted for tooling. + + + + + + + AFF-SEC-213: Logs static strings about DLL discovery; no newline injection vector. + + + + + + AFF-OPS-204: Lock checker must grant cooperative processes access to shared files; permissions and + log output are limited to Chronicle-managed directories until AFF-OPS-330 revisits the design. + + + + + + + AFF-SEC-214: Hardware timer diagnostics are emitted to trusted operator logs; no user-controlled + data flows in. + + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled + (AFF-SEC-310, AFF-OPS-330). + + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled + (AFF-SEC-310, AFF-OPS-330). + + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled + (AFF-SEC-310, AFF-OPS-330). + + + + + + + AFF-SEC-299: package-level suppression for legacy logging patterns; follow-up audit scheduled + (AFF-SEC-310, AFF-OPS-330). + + + + + + + + + + + + diff --git a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java index 608af267d..a9f2810d0 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -22,8 +22,8 @@ import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.LongByReference; -import net.openhft.affinity.IAffinity; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import net.openhft.affinity.IAffinity; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java index 6a19e724f..0dfd9e7c7 100644 --- a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java @@ -23,10 +23,10 @@ import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.nio.file.OpenOption; import java.nio.file.attribute.FileAttribute; diff --git a/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java index ad7eaaa10..7ccc2398c 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java @@ -22,9 +22,7 @@ import java.util.BitSet; import java.util.Locale; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class WindowsJNAAffinityStubTest { diff --git a/pom.xml b/pom.xml index e08693cd9..6914446fd 100644 --- a/pom.xml +++ b/pom.xml @@ -15,14 +15,15 @@ ~ limitations under the License. --> - + 4.0.0 net.openhft java-parent-pom 1.27ea1 - + Java-Thread-Affinity From f91530502a22668bc262d077a08d49b1deabc2af Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 11:00:36 +0000 Subject: [PATCH 11/20] Fix formatting for C/C++ references in requirements and README documents --- README.adoc | 9 +++++---- affinity/src/main/adoc/requirements.adoc | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/README.adoc b/README.adoc index 06e3fd2da..f685a3c0d 100644 --- a/README.adoc +++ b/README.adoc @@ -181,7 +181,7 @@ try (final AffinityLock al = AffinityLock.acquireLock()) { }); t.start(); } ----- +---- In this example, the library will prefer a free CPU on the same Socket as the first thread, otherwise it will pick any free CPU. @@ -225,14 +225,14 @@ The affinity of the process on start up is [source,java] ---- long baseAffinity = AffinityLock.BASE_AFFINITY; ----- +---- The available CPU for reservation is [source,java] ---- long reservedAffinity = AffinityLock.RESERVED_AFFINITY; ----- +---- If you want to get/set the affinity directly you can do @@ -331,7 +331,8 @@ For an article on how much difference affinity can make and how to use it http:/ === Question: How to lock a specific cpuId I am currently working on a project related to deadlock detection in multithreaded programs in java. -We are trying to run threads on different processors and thus came across your github posts regarding the same. https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started +We are trying to run threads on different processors and thus came across your github posts regarding the same. +https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started Being a beginner, I have little knowledge and thus need your assistance. We need to know how to run threads on specified cpu number and then switch threads when one is waiting. diff --git a/affinity/src/main/adoc/requirements.adoc b/affinity/src/main/adoc/requirements.adoc index 009b90f16..000941f8e 100644 --- a/affinity/src/main/adoc/requirements.adoc +++ b/affinity/src/main/adoc/requirements.adoc @@ -1,4 +1,5 @@ = Requirements Document: Java Thread Affinity +:pp: ++ :toc: :sectnums: @@ -151,15 +152,15 @@ No affinity modification; `getCpu()` returns -1. * *FR11.4*: The system _shall_ store meta-information (e.g., PID of the locking process) within the lock file and allow its retrieval. ** `LockChecker.getMetaInfo(int id)` -=== Native Code Compilation (C/C++) +=== Native Code Compilation (C/C{pp}) -* *FR12.1*: The system _shall_ include C/C++ source code for native functions required for affinity and timer operations on Linux and macOS. - ** `software_chronicle_enterprise_internals_impl_NativeAffinity.cpp` (Linux) - ** `software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c` (macOS) - ** `net_openhft_ticker_impl_JNIClock.cpp` (for `rdtsc`) -* *FR12.2*: A Makefile _shall_ be provided to compile the native C/C++ code into a shared library (`libCEInternals.so`). +* *FR12.1*: The system _shall_ include C/C{pp} source code for native functions required for affinity and timer operations on Linux and macOS. +** `software_chronicle_enterprise_internals_impl_NativeAffinity.cpp` (Linux) +** `software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c` (macOS) +** `net_openhft_ticker_impl_JNIClock.cpp` (for `rdtsc`) +* *FR12.2*: A Makefile _shall_ be provided to compile the native C/C{pp} code into a shared library (`libCEInternals.so`). * *FR12.3*: The Java code _shall_ load this native library if available. -** `software.chronicle.enterprise.internals.impl.NativeAffinity.loadAffinityNativeLibrary()` +*** `software.chronicle.enterprise.internals.impl.NativeAffinity.loadAffinityNativeLibrary()` == Non-Functional Requirements @@ -180,7 +181,7 @@ The primary goal is to enable performance improvements in the client application * *NFR5. Build System*: The project _shall_ use Apache Maven for building and dependency management. * *NFR6. Language*: ** Core library _shall_ be implemented in Java (1.8+ as per POM). -** Native components _shall_ be implemented in C/C++. +** Native components _shall_ be implemented in C/C{pp}. * *NFR7. Usability*: ** The API _should_ be clear and relatively simple to use. ** Javadoc _shall_ be provided for public APIs. @@ -218,7 +219,7 @@ It abstracts OS-specific system calls related to thread affinity, CPU informatio ** `SystemClock`: Uses `System.nanoTime()`. * *`net.openhft.affinity.lockchecker.LockChecker`*: Interface for inter-process lock management. ** `FileLockBasedLockChecker`: Implementation using file system locks. -* *Native Code (`src/main/c`)*: C/C++ sources for `libCEInternals.so` providing functions like `getAffinity0`, `setAffinity0` (Linux JNI), `rdtsc0`. +* *Native Code (`src/main/c`)*: C/C{pp} sources for `libCEInternals.so` providing functions like `getAffinity0`, `setAffinity0` (Linux JNI), `rdtsc0`. === Maven Modules From 829afc8968a648a75dabaaf9cbbbb4cb95409f8f Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 11:00:36 +0000 Subject: [PATCH 12/20] Fix formatting for C/C++ references in requirements and README documents --- affinity/pom.xml | 6 +++--- .../java/net/openhft/affinity/MicroJitterSamplerTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/affinity/pom.xml b/affinity/pom.xml index 8772bdbab..8b48a51fa 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -37,8 +37,8 @@ src/main/c UTF-8 3.6.0 - 10.26.1 - 4.9.8.1 + 8.45.1 + 4.8.6.6 1.14.0 3.28.0 0.8.14 @@ -86,7 +86,7 @@ com.github.spotbugs spotbugs-annotations - 4.9.0 + 4.7.3 provided diff --git a/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java b/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java index 593fa4c02..cbc51d91f 100644 --- a/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java +++ b/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java @@ -83,7 +83,7 @@ public void printFormatsCountsPerHour() throws Exception { TOTAL_TIME_FIELD.setLong(sampler, 3_600_000_000_000L); // 1 hour in ns ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (PrintStream ps = new PrintStream(baos, true, StandardCharsets.UTF_8)) { + try (PrintStream ps = new PrintStream(baos, true, StandardCharsets.UTF_8.name())) { sampler.print(ps); } From 82a7a072dec9a7ccee6aabaccfc924959649ada9 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Tue, 28 Oct 2025 12:25:30 +0000 Subject: [PATCH 13/20] Align spotbugs annotations with JDK8 --- affinity-test/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/affinity-test/pom.xml b/affinity-test/pom.xml index 450f04bd7..93393893b 100644 --- a/affinity-test/pom.xml +++ b/affinity-test/pom.xml @@ -36,8 +36,8 @@ UTF-8 3.6.0 - 10.26.1 - 4.9.8.1 + 8.45.1 + 4.8.6.6 1.14.0 3.28.0 0.8.14 From 29b7f4a78aa38c982eb86da42c08e9404526c9df Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 13:33:08 +0000 Subject: [PATCH 14/20] Refactor SpotBugs exclusions and remove deprecated @SuppressFBWarnings annotations --- affinity/src/main/java/net/openhft/affinity/Affinity.java | 2 -- .../main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java | 2 -- 2 files changed, 4 deletions(-) diff --git a/affinity/src/main/java/net/openhft/affinity/Affinity.java b/affinity/src/main/java/net/openhft/affinity/Affinity.java index dd500b880..b84c782cf 100644 --- a/affinity/src/main/java/net/openhft/affinity/Affinity.java +++ b/affinity/src/main/java/net/openhft/affinity/Affinity.java @@ -18,7 +18,6 @@ package net.openhft.affinity; import com.sun.jna.Native; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.affinity.impl.*; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -35,7 +34,6 @@ * * @author peter.lawrey */ -@SuppressFBWarnings(value = {"CRLF_INJECTION_LOGS", "INFORMATION_EXPOSURE_THROUGH_AN_ERROR_MESSAGE"}, justification = "AFF-SEC-205: logging only exposes JVM-local diagnostics for operators") public enum Affinity { ; // none static final Logger LOGGER = LoggerFactory.getLogger(Affinity.class); diff --git a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java index a9f2810d0..65c6e7341 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -22,7 +22,6 @@ import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.LongByReference; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.affinity.IAffinity; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -211,7 +210,6 @@ private interface CLibrary extends Library { int GetCurrentThread() throws LastErrorException; } - @SuppressFBWarnings(value = "NM_METHOD_NAMING_CONVENTION", justification = "Method names must mirror WinAPI signatures for JNA compatibility") private static final class StubWindowsCLibrary implements CLibrary { private long mask = 1L; From 378ff5e5e853a9aae27660bb74bc14a38f8a9daf Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 13:44:35 +0000 Subject: [PATCH 15/20] Create Posix-backed shared test module --- affinity-posix-test-support/pom.xml | 36 +++++++++++++++++++ .../posix/testsupport/PosixTestSupport.java | 27 ++++++++++++++ .../testsupport/PosixTestSupportTest.java | 18 ++++++++++ pom.xml | 1 + 4 files changed, 82 insertions(+) create mode 100644 affinity-posix-test-support/pom.xml create mode 100644 affinity-posix-test-support/src/main/java/net/openhft/affinity/posix/testsupport/PosixTestSupport.java create mode 100644 affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java diff --git a/affinity-posix-test-support/pom.xml b/affinity-posix-test-support/pom.xml new file mode 100644 index 000000000..3c39157f6 --- /dev/null +++ b/affinity-posix-test-support/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + net.openhft + Java-Thread-Affinity + 3.27ea2-SNAPSHOT + + + affinity-posix-test-support + OpenHFT/Java-Thread-Affinity/posix-test-support + Shared test fixtures for Java Thread Affinity built on the Posix library + jar + + + + net.openhft + affinity + ${project.version} + + + net.openhft + posix + 2.27ea3-SNAPSHOT + + + junit + junit + 4.13.2 + test + + + diff --git a/affinity-posix-test-support/src/main/java/net/openhft/affinity/posix/testsupport/PosixTestSupport.java b/affinity-posix-test-support/src/main/java/net/openhft/affinity/posix/testsupport/PosixTestSupport.java new file mode 100644 index 000000000..ada51686f --- /dev/null +++ b/affinity-posix-test-support/src/main/java/net/openhft/affinity/posix/testsupport/PosixTestSupport.java @@ -0,0 +1,27 @@ +package net.openhft.affinity.posix.testsupport; + +import net.openhft.posix.PosixAPI; + +/** + * Lightweight bridge for JVMs that want to share Posix-backed test fixtures. + * This module keeps Posix on the classpath beside the affinity code base, + * enabling future reuse without forcing the main runtime code to depend on it yet. + */ +public final class PosixTestSupport { + + private PosixTestSupport() { + // utility + } + + /** + * Returns a Posix API handle if one can be loaded, otherwise null. + * Tests can use this to decide whether to exercise Posix-backed behaviour. + */ + public static PosixAPI tryLoadPosix() { + try { + return PosixAPI.posix(); + } catch (Throwable ignored) { + return null; + } + } +} diff --git a/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java b/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java new file mode 100644 index 000000000..a102eba50 --- /dev/null +++ b/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java @@ -0,0 +1,18 @@ +package net.openhft.affinity.posix.testsupport; + +import net.openhft.posix.PosixAPI; +import org.junit.Test; + +import static org.junit.Assert.assertNotNull; + +public class PosixTestSupportTest { + + @Test + public void canAttemptToLoadPosix() { + // Even if the runtime cannot provide a concrete implementation, the call should succeed + // without throwing, enabling higher-level tests to branch on null. + PosixAPI api = PosixTestSupport.tryLoadPosix(); + assertNotNull("PosixAPI handle should be returned when the runtime offers an implementation", + api); + } +} diff --git a/pom.xml b/pom.xml index 6914446fd..d83067295 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ affinity + affinity-posix-test-support affinity-test From 3b8d20ad6c38570cfcb8fca8ed3587bb58dd326a Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 13:51:31 +0000 Subject: [PATCH 16/20] Duplicate CPU mask utilities and keep in sync with Posix --- .../internal/util/CpuMaskConversion.java | 43 ++++++++++++++ .../testsupport/PosixTestSupportTest.java | 57 +++++++++++++++++-- .../net/openhft/affinity/impl/CpuSetUtil.java | 20 ++----- .../duplicated/CpuMaskConversion.java | 43 ++++++++++++++ 4 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 affinity-posix-test-support/src/main/java/net/openhft/posix/internal/util/CpuMaskConversion.java create mode 100644 affinity/src/main/java/net/openhft/affinity/internal/duplicated/CpuMaskConversion.java diff --git a/affinity-posix-test-support/src/main/java/net/openhft/posix/internal/util/CpuMaskConversion.java b/affinity-posix-test-support/src/main/java/net/openhft/posix/internal/util/CpuMaskConversion.java new file mode 100644 index 000000000..ca600c3a2 --- /dev/null +++ b/affinity-posix-test-support/src/main/java/net/openhft/posix/internal/util/CpuMaskConversion.java @@ -0,0 +1,43 @@ +package net.openhft.posix.internal.util; + +import java.util.Arrays; +import java.util.BitSet; + +/** + * DUPLICATED with net.openhft.affinity.internal.duplicated.CpuMaskConversion. + *

+ * This class is housed in the shared test-support module for now so that both the Posix and + * Java Thread Affinity code paths can evolve independently while staying in sync. Once Posix + * ships the canonical implementation this duplicate should be removed and the affinity module + * should consume it directly. + */ +public final class CpuMaskConversion { + + private CpuMaskConversion() { + } + + public static int requiredBytesForLogicalProcessors(int logicalProcessors) { + long processors = Math.max(1L, logicalProcessors); + long groups = (processors + Long.SIZE - 1) / Long.SIZE; + long bytes = Math.max(1L, groups) * Long.BYTES; + if (bytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException("CPU mask size exceeds integer addressable space"); + } + return (int) bytes; + } + + public static int requiredBytesForMask(BitSet mask, int logicalProcessorsHint) { + int requiredBits = Math.max(1, Math.max(mask.length(), logicalProcessorsHint)); + return requiredBytesForLogicalProcessors(requiredBits); + } + + public static void writeMask(BitSet affinity, byte[] target) { + Arrays.fill(target, (byte) 0); + byte[] source = affinity.toByteArray(); + System.arraycopy(source, 0, target, 0, Math.min(source.length, target.length)); + } + + public static BitSet readMask(byte[] source) { + return BitSet.valueOf(source); + } +} diff --git a/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java b/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java index a102eba50..5c75b196f 100644 --- a/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java +++ b/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java @@ -3,16 +3,65 @@ import net.openhft.posix.PosixAPI; import org.junit.Test; +import java.util.BitSet; +import java.util.Random; + +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; public class PosixTestSupportTest { @Test public void canAttemptToLoadPosix() { - // Even if the runtime cannot provide a concrete implementation, the call should succeed - // without throwing, enabling higher-level tests to branch on null. PosixAPI api = PosixTestSupport.tryLoadPosix(); - assertNotNull("PosixAPI handle should be returned when the runtime offers an implementation", - api); + assertNotNull("PosixAPI handle should be returned when the runtime offers an implementation", api); + } + + @Test + public void cpuMaskConversionsStayInSync() { + int[] processorCounts = {0, 1, 2, 7, 8, 31, 32, 33, 63, 64, 65, 255, 256}; + for (int count : processorCounts) { + int affinityBytes = net.openhft.affinity.internal.duplicated.CpuMaskConversion.requiredBytesForLogicalProcessors(count); + int posixBytes = net.openhft.posix.internal.util.CpuMaskConversion.requiredBytesForLogicalProcessors(count); + assertEquals("Byte requirement mismatch for processor count " + count, affinityBytes, posixBytes); + } + + Random random = new Random(1234L); + for (int logicalHint : processorCounts) { + BitSet mask = randomBitSet(random, logicalHint + 32); + int affinityBytes = net.openhft.affinity.internal.duplicated.CpuMaskConversion.requiredBytesForMask(mask, logicalHint); + int posixBytes = net.openhft.posix.internal.util.CpuMaskConversion.requiredBytesForMask(mask, logicalHint); + assertEquals("Mask byte requirement mismatch", affinityBytes, posixBytes); + + byte[] affinityBuffer = new byte[affinityBytes]; + byte[] posixBuffer = new byte[posixBytes]; + net.openhft.affinity.internal.duplicated.CpuMaskConversion.writeMask(mask, affinityBuffer); + net.openhft.posix.internal.util.CpuMaskConversion.writeMask(mask, posixBuffer); + assertEquals("Written mask mismatch", toBitString(affinityBuffer), toBitString(posixBuffer)); + + BitSet fromAffinity = net.openhft.affinity.internal.duplicated.CpuMaskConversion.readMask(affinityBuffer); + BitSet fromPosix = net.openhft.posix.internal.util.CpuMaskConversion.readMask(posixBuffer); + assertEquals("Reconstructed mask mismatch", fromAffinity, fromPosix); + } + } + + private static BitSet randomBitSet(Random random, int maxBits) { + BitSet bitSet = new BitSet(maxBits); + for (int i = 0; i < maxBits; i++) { + if (random.nextBoolean()) { + bitSet.set(i); + } + } + return bitSet; + } + + private static String toBitString(byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 8); + for (byte b : data) { + for (int i = 0; i < 8; i++) { + sb.append((b >> i) & 1); + } + } + return sb.toString(); } } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java index e9f1a6c7e..c3bb133f0 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java @@ -1,6 +1,7 @@ package net.openhft.affinity.impl; -import java.util.Arrays; +import net.openhft.affinity.internal.duplicated.CpuMaskConversion; + import java.util.BitSet; /** @@ -12,27 +13,18 @@ private CpuSetUtil() { } static int requiredBytesForLogicalProcessors(int logicalProcessors) { - long processors = Math.max(1L, logicalProcessors); - long groups = (processors + Long.SIZE - 1) / Long.SIZE; - long bytes = Math.max(1L, groups) * Long.BYTES; - if (bytes > Integer.MAX_VALUE) { - throw new IllegalArgumentException("CPU mask size exceeds integer addressable space"); - } - return (int) bytes; + return CpuMaskConversion.requiredBytesForLogicalProcessors(logicalProcessors); } static int requiredBytesForMask(BitSet mask, int logicalProcessorsHint) { - int requiredBits = Math.max(1, Math.max(mask.length(), logicalProcessorsHint)); - return requiredBytesForLogicalProcessors(requiredBits); + return CpuMaskConversion.requiredBytesForMask(mask, logicalProcessorsHint); } static void writeMask(BitSet affinity, byte[] target) { - Arrays.fill(target, (byte) 0); - byte[] source = affinity.toByteArray(); - System.arraycopy(source, 0, target, 0, Math.min(source.length, target.length)); + CpuMaskConversion.writeMask(affinity, target); } static BitSet readMask(byte[] source) { - return BitSet.valueOf(source); + return CpuMaskConversion.readMask(source); } } diff --git a/affinity/src/main/java/net/openhft/affinity/internal/duplicated/CpuMaskConversion.java b/affinity/src/main/java/net/openhft/affinity/internal/duplicated/CpuMaskConversion.java new file mode 100644 index 000000000..35e836cf1 --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/internal/duplicated/CpuMaskConversion.java @@ -0,0 +1,43 @@ +package net.openhft.affinity.internal.duplicated; + +import java.util.Arrays; +import java.util.BitSet; + +/** + * DUPLICATED with net.openhft.posix.internal.util.CpuMaskConversion. + *

+ * This temporary duplication keeps the affinity module independent of the Posix module + * while allowing shared tests to assert both implementations behave identically. + * Once affinity adopts Posix as its native backend this class can be deleted and consumers + * should migrate to the Posix version. + */ +public final class CpuMaskConversion { + + private CpuMaskConversion() { + } + + public static int requiredBytesForLogicalProcessors(int logicalProcessors) { + long processors = Math.max(1L, logicalProcessors); + long groups = (processors + Long.SIZE - 1) / Long.SIZE; + long bytes = Math.max(1L, groups) * Long.BYTES; + if (bytes > Integer.MAX_VALUE) { + throw new IllegalArgumentException("CPU mask size exceeds integer addressable space"); + } + return (int) bytes; + } + + public static int requiredBytesForMask(BitSet mask, int logicalProcessorsHint) { + int requiredBits = Math.max(1, Math.max(mask.length(), logicalProcessorsHint)); + return requiredBytesForLogicalProcessors(requiredBits); + } + + public static void writeMask(BitSet affinity, byte[] target) { + Arrays.fill(target, (byte) 0); + byte[] source = affinity.toByteArray(); + System.arraycopy(source, 0, target, 0, Math.min(source.length, target.length)); + } + + public static BitSet readMask(byte[] source) { + return BitSet.valueOf(source); + } +} From 902b4a801f31cbc103a93283a7903925f0ae7077 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 15:37:05 +0000 Subject: [PATCH 17/20] Suppress SpotBugs naming warnings for Windows stub --- .../main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java index 65c6e7341..18f7e8188 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -22,6 +22,7 @@ import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.LongByReference; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.affinity.IAffinity; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -210,6 +211,7 @@ private interface CLibrary extends Library { int GetCurrentThread() throws LastErrorException; } + @SuppressFBWarnings(value = "NM_METHOD_NAMING_CONVENTION", justification = "Method names mirror WinAPI declarations for JNA compatibility and are intentionally PascalCase") private static final class StubWindowsCLibrary implements CLibrary { private long mask = 1L; From 8aabe21f958cb5b4a62b45f6c15dd370bbc6eb4d Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 15:44:57 +0000 Subject: [PATCH 18/20] Route Windows stub exclusions through SpotBugs filter --- affinity/src/main/config/spotbugs-exclude.xml | 4 ++++ .../java/net/openhft/affinity/impl/WindowsJNAAffinity.java | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/affinity/src/main/config/spotbugs-exclude.xml b/affinity/src/main/config/spotbugs-exclude.xml index 0c3946b4f..893a4018c 100644 --- a/affinity/src/main/config/spotbugs-exclude.xml +++ b/affinity/src/main/config/spotbugs-exclude.xml @@ -113,6 +113,10 @@ AFF-SEC-213: Logs static strings about DLL discovery; no newline injection vector. + + + + diff --git a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java index 18f7e8188..65c6e7341 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -22,7 +22,6 @@ import com.sun.jna.platform.win32.WinDef; import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.LongByReference; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import net.openhft.affinity.IAffinity; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -211,7 +210,6 @@ private interface CLibrary extends Library { int GetCurrentThread() throws LastErrorException; } - @SuppressFBWarnings(value = "NM_METHOD_NAMING_CONVENTION", justification = "Method names mirror WinAPI declarations for JNA compatibility and are intentionally PascalCase") private static final class StubWindowsCLibrary implements CLibrary { private long mask = 1L; From b8a0a6d22ec12836e7b86d3a1a55ac5c1b7d5f8d Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Wed, 29 Oct 2025 15:47:33 +0000 Subject: [PATCH 19/20] Remove unused SpotBugs annotations dependency from pom.xml --- affinity/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/affinity/pom.xml b/affinity/pom.xml index 8b48a51fa..b9039af61 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -83,12 +83,6 @@ org.jetbrains annotations - - com.github.spotbugs - spotbugs-annotations - 4.7.3 - provided - org.easymock From dc7562ea954d2bd4aebf4d3f2f43072abbfa86a3 Mon Sep 17 00:00:00 2001 From: Peter Lawrey Date: Thu, 30 Oct 2025 10:59:53 +0000 Subject: [PATCH 20/20] Move Checkstyle config under src/main/config --- affinity-test/pom.xml | 2 +- affinity-test/src/main/config/checkstyle.xml | 210 +++++++++++++++++++ affinity/pom.xml | 2 +- affinity/src/main/config/checkstyle.xml | 210 +++++++++++++++++++ 4 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 affinity-test/src/main/config/checkstyle.xml create mode 100644 affinity/src/main/config/checkstyle.xml diff --git a/affinity-test/pom.xml b/affinity-test/pom.xml index 93393893b..922efddae 100644 --- a/affinity-test/pom.xml +++ b/affinity-test/pom.xml @@ -225,7 +225,7 @@ - net/openhft/quality/checkstyle/checkstyle.xml + src/main/config/checkstyle.xml true true warning diff --git a/affinity-test/src/main/config/checkstyle.xml b/affinity-test/src/main/config/checkstyle.xml new file mode 100644 index 000000000..844dd904b --- /dev/null +++ b/affinity-test/src/main/config/checkstyle.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/affinity/pom.xml b/affinity/pom.xml index b9039af61..8e7dffb88 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -219,7 +219,7 @@ - net/openhft/quality/checkstyle/checkstyle.xml + src/main/config/checkstyle.xml true true warning diff --git a/affinity/src/main/config/checkstyle.xml b/affinity/src/main/config/checkstyle.xml new file mode 100644 index 000000000..844dd904b --- /dev/null +++ b/affinity/src/main/config/checkstyle.xml @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +