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 61367bd58..f685a3c0d 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,5 @@ = Thread Affinity +:sectnums: image::docs/images/Thread-Affinity_line.png[width=20%] @@ -7,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 @@ -62,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. @@ -79,6 +83,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. @@ -121,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 @@ -145,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. @@ -174,11 +181,12 @@ 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. === Affinity strategies + The `AffinityStrategies` enum defines hints for selecting a CPU relative to an existing lock. [options="header",cols="1,3"] @@ -193,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] @@ -201,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] @@ -215,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 @@ -319,8 +329,10 @@ 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 +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-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/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 new file mode 100644 index 000000000..5c75b196f --- /dev/null +++ b/affinity-posix-test-support/src/test/java/net/openhft/affinity/posix/testsupport/PosixTestSupportTest.java @@ -0,0 +1,67 @@ +package net.openhft.affinity.posix.testsupport; + +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() { + PosixAPI api = PosixTestSupport.tryLoadPosix(); + 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-test/pom.xml b/affinity-test/pom.xml index baa74ad86..922efddae 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 @@ -34,6 +35,15 @@ UTF-8 + 3.6.0 + 8.45.1 + 4.8.6.6 + 1.14.0 + 3.28.0 + 0.8.14 + 0.0 + 0.0 + 1.23ea6 @@ -182,6 +192,147 @@ + + 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 + + + + + src/main/config/checkstyle.xml + true + true + warning + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.version} + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + spotbugs + verify + + check + + + + + Max + Low + true + ../affinity/src/main/config/spotbugs-exclude.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${maven-pmd-plugin.version} + + + pmd + verify + + check + + + + + true + true + src/main/config/pmd-exclude.properties + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + ${jacoco.line.coverage} + + + BRANCH + COVEREDRATIO + ${jacoco.branch.coverage} + + + + + + + + + + + pre-java9 @@ -203,10 +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-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-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/pom.xml b/affinity/pom.xml index b1c9e4561..8e7dffb88 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 @@ -35,6 +36,15 @@ src/main/c UTF-8 + 3.6.0 + 8.45.1 + 4.8.6.6 + 1.14.0 + 3.28.0 + 0.8.14 + 0.0 + 0.0 + 1.23ea6 @@ -153,6 +163,7 @@ org.jacoco jacoco-maven-plugin + ${jacoco-maven-plugin.version} @@ -171,6 +182,152 @@ + + code-review + + false + + + 0.54 + 0.49 + + + + + 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 + + + + + src/main/config/checkstyle.xml + true + true + warning + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs.version} + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + spotbugs + verify + + check + + + + + Max + Low + true + src/main/config/spotbugs-exclude.xml + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs.version} + + + + + + org.apache.maven.plugins + maven-pmd-plugin + ${maven-pmd-plugin.version} + + + pmd + verify + + check + + + + + true + true + ${project.basedir}/src/main/config/pmd-exclude.properties + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + LINE + COVEREDRATIO + ${jacoco.line.coverage} + + + BRANCH + COVEREDRATIO + ${jacoco.branch.coverage} + + + + + + + + + + + diff --git a/affinity/src/main/adoc/requirements.adoc b/affinity/src/main/adoc/requirements.adoc index 8132561a5..000941f8e 100644 --- a/affinity/src/main/adoc/requirements.adoc +++ b/affinity/src/main/adoc/requirements.adoc @@ -1,7 +1,9 @@ = Requirements Document: Java Thread Affinity +:pp: ++ :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 +11,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 +23,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 +34,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 +74,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 +96,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 +110,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 +123,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 +134,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,17 +152,17 @@ 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{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()` -== 7. Non-Functional Requirements +== Non-Functional Requirements * *NFR1. Platform Support*: ** *Primary Support*: Linux (full functionality). @@ -179,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. @@ -193,14 +195,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. @@ -217,9 +219,9 @@ 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`. -=== 8.3. Maven Modules +=== Maven Modules * *`Java-Thread-Affinity` (Parent POM)*: Aggregates sub-modules. ** Group ID: `net.openhft` @@ -231,7 +233,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 +253,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 +275,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 +284,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: 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..893a4018c --- /dev/null +++ b/affinity/src/main/config/spotbugs-exclude.xml @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/Affinity.java b/affinity/src/main/java/net/openhft/affinity/Affinity.java index d4654749a..b84c782cf 100644 --- a/affinity/src/main/java/net/openhft/affinity/Affinity.java +++ b/affinity/src/main/java/net/openhft/affinity/Affinity.java @@ -176,12 +176,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 +222,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/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..733537078 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(); @@ -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/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/CpuSetUtil.java b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java new file mode 100644 index 000000000..c3bb133f0 --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/impl/CpuSetUtil.java @@ -0,0 +1,30 @@ +package net.openhft.affinity.impl; + +import net.openhft.affinity.internal.duplicated.CpuMaskConversion; + +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) { + return CpuMaskConversion.requiredBytesForLogicalProcessors(logicalProcessors); + } + + static int requiredBytesForMask(BitSet mask, int logicalProcessorsHint) { + return CpuMaskConversion.requiredBytesForMask(mask, logicalProcessorsHint); + } + + static void writeMask(BitSet affinity, byte[] target) { + CpuMaskConversion.writeMask(affinity, target); + } + + static BitSet readMask(byte[] source) { + return CpuMaskConversion.readMask(source); + } +} 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..7225616fa 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java @@ -20,28 +20,48 @@ 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); + 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) { - //Jvm.warn().on(getClass(), "Failed to determine Linux version: " + e); } version = ver; @@ -50,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; @@ -71,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(); @@ -95,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) { @@ -132,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) { @@ -145,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) { @@ -158,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; @@ -177,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. */ @@ -236,43 +306,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 @@ -285,7 +353,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/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/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 50b0f299d..144da5a45 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 @@ -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,11 +90,12 @@ 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 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 +104,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); @@ -109,14 +130,22 @@ 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; - byte[] buff = affinity.toByteArray(); - final int cpuSetSizeInBytes = buff.length; + final CLibrary lib = LIBRARY; + 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); @@ -147,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) @@ -178,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; @@ -191,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; @@ -211,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/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/impl/WindowsJNAAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java index c64bd15be..65c6e7341 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -42,14 +42,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 +99,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 +130,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 +156,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 +172,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 +203,35 @@ 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; } + + 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/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); + } +} 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..0dfd9e7c7 100644 --- a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java @@ -26,6 +26,7 @@ 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; @@ -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/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/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/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/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/MicroJitterSamplerTest.java b/affinity/src/test/java/net/openhft/affinity/MicroJitterSamplerTest.java new file mode 100644 index 000000000..cbc51d91f --- /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.name())) { + 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/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/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/impl/LinuxJNAAffinityStubTest.java b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java new file mode 100644 index 000000000..22051dbd2 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityStubTest.java @@ -0,0 +1,67 @@ +/* + * 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.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class LinuxJNAAffinityStubTest { + + @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")); + + 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 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()); + } 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 new file mode 100644 index 000000000..5a04ef50a --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/OSXJNAAffinityStubTest.java @@ -0,0 +1,52 @@ +/* + * 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.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class OSXJNAAffinityStubTest { + + @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")); + + 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/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); + } + } +} 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..c52494d9b --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityStubTest.java @@ -0,0 +1,60 @@ +/* + * 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.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PosixJNAAffinityStubTest { + + @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")); + + 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 new file mode 100644 index 000000000..3fed2ebe2 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/SolarisJNAAffinityStubTest.java @@ -0,0 +1,51 @@ +/* + * 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.Test; + +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class SolarisJNAAffinityStubTest { + + @Test + public void stubReturnsConsistentThreadId() { + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + Assume.assumeFalse("Do not override native Solaris library", osName.contains("sunos")); + + 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 new file mode 100644 index 000000000..7ccc2398c --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/WindowsJNAAffinityStubTest.java @@ -0,0 +1,57 @@ +/* + * 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.Test; + +import java.util.BitSet; +import java.util.Locale; + +import static org.junit.Assert.*; + +public class WindowsJNAAffinityStubTest { + + @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")); + + 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); + } + } + } +} 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); + } + } +} 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); + } +} diff --git a/pom.xml b/pom.xml index e08693cd9..d83067295 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 @@ -34,6 +35,7 @@ affinity + affinity-posix-test-support affinity-test