diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index aa12f475b..00caab13d --- a/.gitignore +++ b/.gitignore @@ -1,27 +1,53 @@ +### How to update +# This is copied from OpenHFT/.gitignore +# update the original and run OpenHFT/update_gitignore.sh + +### Compiled class file *.class -# Package Files # +### Package Files *.jar *.war *.ear -# IntelliJ +### Log file +*.log + +### IntelliJ *.iml *.ipr *.iws .idea +compat_reports +.attach_pid* -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +### Virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* -# Eclipse +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties + +### Eclipse template +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties .classpath .project .settings/ +.loadpath -# maven -target - -# vim -*.swp -*.swo +### Queue files +*.cq4t +*.cq4 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 02bbb60bc..000000000 --- a/LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file diff --git a/LICENSE.adoc b/LICENSE.adoc new file mode 100644 index 000000000..f93a31eb3 --- /dev/null +++ b/LICENSE.adoc @@ -0,0 +1,13 @@ +== 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. diff --git a/README.adoc b/README.adoc new file mode 100644 index 000000000..13720fe77 --- /dev/null +++ b/README.adoc @@ -0,0 +1,378 @@ += Thread Affinity + +image::docs/images/Thread-Affinity_line.png[width=20%] + +== Version + +[#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 + +See https://github.com/OpenHFT/Java-Thread-Affinity/tree/master/affinity/src/test/java[affinity/src/test/java] +for working examples of how to use this library. + +=== Supported operating systems + +The library detects the running platform in `Affinity.java` and selects an implementation for that OS. +Features differ between systems: + +* *Linux* - full affinity control via JNA. +The implementation can get and set thread affinity, query the current CPU, and obtain process and thread IDs. +* *Windows* - thread affinity is managed through the kernel API. +Process and thread IDs are available, while `getCpu()` returns `-1`. +* *macOS* - provides process and thread IDs but does not modify affinity and reports the CPU id as `-1`. +* *Solaris* - mirrors the macOS implementation: only process and thread IDs are returned with no affinity or CPU querying support. + +=== Changes + +* V3.2.0 - Add support for text configuration +* V3.1.1 - Upgraded JNA dependency to 4.4.0 +* V2.0.1 - Added getThreadId for the process if of the thread. + +=== Dependencies + +Java-Thread-Affinity will try to use link:https://github.com/java-native-access/jna[JNA] +to provide access to native thread-handling functions. +JNA should be installed on your system to get the most from this library. + +=== JNA version + +Java-Thread-Affinity currently depends on JNA version 4.4.0, which in turn depends on a version of GLIBC >= 2.14. If your operating system is an old one, with a version of GLIBC released before 2011, this library will not be able to invoke native functions. + +To work around this problem, fork the repository, and override the `` tag for the artifacts `jna` and `jna-platform` in the project's `pom` file. + +=== Installing JNA on Ubuntu + + sudo apt-get install libjna-java + +=== Installing JNA on CentOS + + sudo yum install jna + +=== Installing JNA on Windows + + choco install jna + +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. + +The library looks for isolated CPUs determined by looking at the CPUs you are not running on by default. +i.e. if you have 16 CPUs but 8 of them are not available for general use (as determined by the affinity of the process on startup) it will start assigning to those CPUs. + +Note: if you have more than one process using this library you need to specify which CPUs the process can use otherwise it will assign the same CPUs to both processes. + +To control which CPUs a process can use, add `-Daffinity.reserved={cpu-mask-in-hex}` to the command line of the process. +The mask is a hexadecimal bit mask without the `0x` prefix where bit `0` represents CPU `0`, bit `1` represents CPU `1` and so on. +Multiple CPUs can be specified by setting more than one bit. + +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`). + +Use an appropriate mask when starting each process to avoid reserving the same cores for multiple JVMs. + +Note: the CPU 0 is reserved for the Operating System, it has to run somewhere. + +=== References + +https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started + +https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/How-it-works + +https://vanillajava.blogspot.com/2013/07/micro-jitter-busy-waiting-and-binding.html + +=== isolcpus + +Java-Thread-Affinity requires that you first isolate some CPU's. + +Once a CPU core is isolated, the Linux scheduler will not use the CPU core to run any user-space processes. +The isolated CPUs will not participate in load balancing, and will not have tasks running on them unless explicitly assigned. + +To isolate the 1st and 3rd CPU cores (CPU numbers start from 0) on your system, add the following to the kernel command line during boot: + +isolcpus=1,3 + +.Using GRUB +[source] +---- +sudo sed -i 's/^GRUB_CMDLINE_LINUX_DEFAULT="/GRUB_CMDLINE_LINUX_DEFAULT="isolcpus=1,3 /' /etc/default/grub +sudo update-grub +sudo reboot +---- + +.Using systemd-boot +[source] +---- +sudo sed -i 's/^options \(.*\)/options \1 isolcpus=1,3/' /boot/loader/entries/*.conf +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 +[source,java] +---- +AffinityLock al = AffinityLock.acquireLock(); +try { + // do some work locked to a CPU. +} finally { + al.release(); +} +---- + +.In Java 7 or 8 +[source,java] +---- +try (AffinityLock al = AffinityLock.acquireLock()) { + // do some work while locked to a CPU. +} +---- + +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. + +[source,java] +---- +try (AffinityLock al = AffinityLock.acquireCore()) { + // do some work while locked to a CPU. +} +---- + +=== Controlling layout + +You can chose a layout relative to an existing lock. + +[source,java] +---- +try (final AffinityLock al = AffinityLock.acquireLock()) { + System.out.println("Main locked"); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try (AffinityLock al2 = al.acquireLock(AffinityStrategies.SAME_SOCKET, + AffinityStrategies.ANY)) { + System.out.println("Thread-0 locked"); + } + } + }); + 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"] +|=== +| Strategy | Meaning + +|`ANY`|Use any available CPU. +|`SAME_CORE`|Select a CPU on the same core. +|`SAME_SOCKET`|Select a CPU on the same socket but a different core. +|`DIFFERENT_CORE`|Select a CPU on another core (possibly another socket). +|`DIFFERENT_SOCKET`|Select a CPU on a different socket. +|=== + +=== Getting the thread id +You can get the current thread id using + +[source,java] +---- +int threadId = AffinitySupport.getThreadId(); +---- + +=== Determining which CPU you are running on +You can get the current CPU being used by + +[source,java] +---- +int cpuId = AffinitySupport.getCpu(); +---- + +=== Controlling the affinity more directly + +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 + +[source,java] +---- +long currentAffinity = AffinitySupport.getAffinity(); +AffinitySupport.setAffinity(1L << 5); // lock to CPU 5. +---- + +=== Understanding dumpLocks() output + +Several examples print the current CPU assignments using `AffinityLock.dumpLocks()`. +Each line of the output begins with the zero based CPU id followed by the status of that CPU. +Example output might look like: + +[source] +---- +0: Reserved for this application +1: Thread[reader,5,main] alive=true +2: General use CPU +3: CPU not available +---- + +The number on each line is the logical CPU index as recognised by the library. +The text after the colon describes whether that CPU is free, reserved or already bound to a thread. +Use these indices when calling `AffinityLock.acquireLock(n)` +or when constructing explicit affinity masks. + +=== Lock file directory + +AffinityLock stores a small lock file for each CPU. +These files are placed in the directory specified by the `java.io.tmpdir` system property, which by default points to your system's temporary directory (usually `/tmp` on Linux). + +If you want to keep the lock files elsewhere, set this property before using any affinity APIs: + +[source,bash] +---- +java -Djava.io.tmpdir=/path/to/dir ... +---- + +or in code + +[source,java] +---- +System.setProperty("java.io.tmpdir", "/path/to/dir"); +---- + +=== Debugging affinity state + +For a detailed of view of the current affinity state (as seen by the library), execute the following script on Linux systems: + +[source] +---- +# change to the affinity lock-file directory (defaults to system property java.io.tmpdir) +$ cd /tmp + +# dump affinity state +$ for i in "$(ls cpu-*)"; + do PID="$(cat $i | head -n1)"; TIMESTAMP="$(cat $i | tail -n1)"; + echo "pid $PID locked at $TIMESTAMP in $i"; taskset -cp $PID; + cat "/proc/$PID/cmdline"; echo; echo + done + + pid 14584 locked at 2017.10.30 at 10:33:24 GMT in cpu-3.lock + pid 14584's current affinity list: 3 + /opt/jdk1.8.0_141/bin/java ... + +---- + +== Using AffinityThreadFactory + +`AffinityThreadFactory` binds each thread it creates according to a set of `AffinityStrategy` rules. +This allows executors to automatically run tasks on cores selected by the library. + +[source,java] +---- +ExecutorService es = Executors.newFixedThreadPool(4, + new AffinityThreadFactory("worker", + AffinityStrategies.SAME_CORE, + AffinityStrategies.DIFFERENT_SOCKET, + AffinityStrategies.ANY)); +es.submit(() -> { + // your task here +}); +System.out.println("\nThe assignment of CPUs is\n" + AffinityLock.dumpLocks()); +---- + +== Support Material + +https://groups.google.com/forum/?hl=en-GB#!forum/java-thread-affinity[Java Thread Affinity support group] + +For an article on how much difference affinity can make and how to use it http://vanillajava.blogspot.com/2013/07/micro-jitter-busy-waiting-and-binding.html + +== Questions and Answers + +=== Question: How to lock a specific cpuId +I am currently working on a project related to deadlock detection in multithreaded programs in java. +We are trying to run threads on different processors and thus came across your github posts regarding the same. https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started +Being a beginner, I have little knowledge and thus need your assistance. +We need to know how to run threads on specified cpu number and then switch threads when one is waiting. + +=== Answer + +[source,java] +---- +// lock a cpuId +try (AffinityLock lock = AffinityLock.acquireLock(n)) { + +} +---- + +where n is the cpu you want to run the thread on. + +OR + +[source,java] +---- +// lock one of the last CPUs +try (AffinityLock lock = AffinityLock.acquireLockLastMinus(n)) { + +} +---- + +=== Question: how to use a configuration file to set the cpuId + +I have the cpuId in a configuration file, how can I set it using a string? + +=== Answer: use one of the following + +[source,java] +---- +try (AffinityLock lock = AffinityLock.acquireLock("last")) { + assertEquals(PROCESSORS - 1, Affinity.getCpu()); +} +try (AffinityLock lock = AffinityLock.acquireLock("last-1")) { + assertEquals(PROCESSORS - 2, Affinity.getCpu()); +} +try (AffinityLock lock = AffinityLock.acquireLock("1")) { + assertEquals(1, Affinity.getCpu()); +} +try (AffinityLock lock = AffinityLock.acquireLock("any")) { + assertTrue(lock.bound); +} +try (AffinityLock lock = AffinityLock.acquireLock("none")) { + assertFalse(lock.bound); +} +try (AffinityLock lock = AffinityLock.acquireLock((String) null)) { + assertFalse(lock.bound); +} +try (AffinityLock lock = AffinityLock.acquireLock("0")) { // prints a warning + assertFalse(lock.bound); +} +---- diff --git a/README.md b/README.md deleted file mode 100644 index c679b044b..000000000 --- a/README.md +++ /dev/null @@ -1,117 +0,0 @@ -Java-Affinity -============= - -OpenHFT Java Thread Affinity library - -See the affinity/src/test/java for working examples of how to use this library. - -## Versions - -V2.2 - Latest build. -V2.0.1 - Added getThreadId for the process if of the thread. - -## How does 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 Socket. - -The library looks for isolated CPUs determined by looking at the CPUs you are not running on by default. -i.e. if you have 16 CPUs but 8 of them are not available for general use (as determined by the affinity of the process on startup) it will start assigning to those CPUs. - -Note: if you have more than one process using this library you need to specify which CPUs the process can use otherwise it will assign the same CPUs to both processes. -To control which CPUs a process can use, add -Daffinity.reserved={cpu-mark-in-hex} to the command line of the process - -Note: the CPU 0 is reserved for the Operating System, it has to run somewhere. - -## Acquiring a CPU lock for a thread -You can acquire a lock for a CPU in the following matter - -In Java 6 - - AffinityLock al = AffinityLock.acquireLock(); - try { - // do some work locked to a CPU. - } finally { - al.release(); - } - -In Java 7 or 8 - - try (AffinityLock al = AffinityLock.acquireLock()) { - // do some work while locked to a CPU. - } - -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. - - try (AffinityLock al = AffinityLock.acquireCore()) { - // do some work while locked to a CPU. - } - -## Controlling layout -You can chose a layout relative to an existing lock. - - try (final AffinityLock al = AffinityLock.acquireLock()) { - System.out.println("Main locked"); - Thread t = new Thread(new Runnable() { - @Override - public void run() { - try (AffinityLock al2 = al.acquireLock(AffinityStrategies.SAME_SOCKET, - AffinityStrategies.ANY)) { - System.out.println("Thread-0 locked"); - } - } - }); - 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. - -## Getting the thread id. -You can get the current thread id using - - int threadId = AffinitySupport.getThreadId(); - -## determining which CPU you are running on. -You can get the current CPU being used by - - int cpuId = AffinitySupport.getCpu(); - -## Controlling the affinity more directly. -The affinity of the process on start up is - - long baseAffinity = AffinityLock.BASE_AFFINITY; - -The available CPU for reservation is - - long reservedAffinity = AffinityLock.RESERVED_AFFINITY; - -If you want to get/set the affinity directly you can do - - long currentAffinity = AffinitySupport.getAffinity(); - AffinitySupport.setAffinity(1L << 5); // lock to CPU 5. - -# Support Material - -[Java Thread Affinity support group](https://groups.google.com/forum/?hl=en-GB#!forum/java-thread-affinity) - -For an article on how much difference affinity can make and how to use it http://vanillajava.blogspot.com/2013/07/micro-jitter-busy-waiting-and-binding.html - -# Questions and Answers - -## Question -I am currently working on a project related to deadlock detection in multithreaded programs in java. We are trying to run threads on different processors and thus came across your github posts regarding the same. https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started -Being a beginner, I have little knowledge and thus need your assistance. We need to know how to run threads on specified cpu number and then switch threads when one is waiting. - -## Answer - -Use : - -``` java -AffinityLock.setAffinity (1L << n); -``` - -where n is the cpu you want to run the thread on. - - - diff --git a/affinity-test/pom.xml b/affinity-test/pom.xml index d0ec7ca39..3ce950e34 100644 --- a/affinity-test/pom.xml +++ b/affinity-test/pom.xml @@ -1,34 +1,22 @@ - - + + Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + +--> + + + 4.0.0 net.openhft java-parent-pom - 1.1.4 - + 2026.0 + - 4.0.0 affinity-test - 3.0-SNAPSHOT + 2026.3-SNAPSHOT bundle OpenHFT/Java-Thread-Affinity/affinity-test @@ -36,15 +24,17 @@ UTF-8 + 0 + 0 - + net.openhft third-party-bom + 2026.0 pom - 3.4.11 import @@ -56,17 +46,13 @@ - - com.sun.java - tools - net.openhft affinity - javax.inject - javax.inject + jakarta.inject + jakarta.inject-api test @@ -108,7 +94,6 @@ org.apache.felix org.apache.felix.framework - 4.4.1 test @@ -130,8 +115,8 @@ maven-compiler-plugin -Xlint:deprecation - 1.7 - 1.7 + 1.8 + 1.8 UTF-8 @@ -188,9 +173,32 @@ + + + pre-java9 + + + com.sun + tools + 1.8.0 + system + ${java.home}/../lib/tools.jar + + + + + ${java.home}/../lib/tools.jar + + + + + - scm:git:https://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/java/net/openhft/affinity/osgi/OSGiPlaceholder.java b/affinity-test/src/main/java/net/openhft/affinity/osgi/OSGiPlaceholder.java index 896a8270e..a6e7ef36d 100644 --- a/affinity-test/src/main/java/net/openhft/affinity/osgi/OSGiPlaceholder.java +++ b/affinity-test/src/main/java/net/openhft/affinity/osgi/OSGiPlaceholder.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.osgi; /** diff --git a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java index 78d0891b4..a3213a6e7 100644 --- a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java +++ b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java @@ -1,21 +1,9 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.osgi; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.ops4j.pax.exam.Configuration; @@ -29,27 +17,27 @@ import static org.junit.Assert.*; import static org.ops4j.pax.exam.CoreOptions.*; -//@Ignore +@Ignore("TODO FIX") @RunWith(PaxExam.class) public class OSGiBundleTest extends net.openhft.affinity.osgi.OSGiTestBase { @Inject - BundleContext context; + private BundleContext context; @Configuration public Option[] config() { return options( - systemProperty("org.osgi.framework.storage.clean").value("true"), - systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("WARN"), - mavenBundleAsInProject("org.slf4j","slf4j-api"), - mavenBundleAsInProject("org.slf4j","slf4j-simple").noStart(), - mavenBundleAsInProject("net.openhft","affinity"), - workspaceBundle("affinity-test"), - junitBundles(), - systemPackage("sun.misc"), - systemPackage("sun.nio.ch"), - systemPackage("com.sun.jna"), - systemPackage("com.sun.jna.ptr"), - cleanCaches() + systemProperty("org.osgi.framework.storage.clean").value("true"), + systemProperty("org.ops4j.pax.logging.DefaultServiceLog.level").value("WARN"), + mavenBundleAsInProject("org.slf4j", "slf4j-api"), + mavenBundleAsInProject("org.slf4j", "slf4j-simple").noStart(), + mavenBundleAsInProject("net.openhft", "affinity"), + workspaceBundle("affinity-test"), + junitBundles(), + systemPackage("sun.misc"), + systemPackage("sun.nio.ch"), + systemPackage("com.sun.jna"), + systemPackage("com.sun.jna.ptr"), + cleanCaches() ); } @@ -57,26 +45,26 @@ public Option[] config() { public void checkInject() { assertNotNull(context); } - + @Test public void checkBundleState() { final Bundle bundle = findBundle(context, "net.openhft.affinity"); assertNotNull(bundle); - assertEquals(bundle.getState(),Bundle.ACTIVE); + assertEquals(Bundle.ACTIVE, bundle.getState()); } - + @Test public void checkBundleExports() { final Bundle bundle = findBundle(context, "net.openhft.affinity"); assertNotNull(bundle); - + final String exports = bundle.getHeaders().get("Export-Package"); final String[] packages = exports.split(","); - + assertTrue(packages.length >= 2); - assertTrue(packages[0].startsWith("net.openhft.affinity;") + assertTrue(packages[0].startsWith("net.openhft.affinity;") || packages[0].startsWith("net.openhft.affinity.impl;")); - assertTrue(packages[1].startsWith("net.openhft.affinity;") + assertTrue(packages[1].startsWith("net.openhft.affinity;") || packages[1].startsWith("net.openhft.affinity.impl;")); } } diff --git a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java index 53cab1e79..a5fb702dd 100644 --- a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java +++ b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.osgi; import org.ops4j.pax.exam.CoreOptions; @@ -24,30 +11,30 @@ import java.io.File; -public class OSGiTestBase { - - public static Option workspaceBundle(String projectName) { +class OSGiTestBase { + + static Option workspaceBundle(String projectName) { String baseDir = System.getProperty("main.basedir"); - String bundleDir = null; + String bundleDir; - bundleDir = String.format("%s/%s/target/classes",baseDir,projectName); - if(new File(bundleDir).exists()) { + bundleDir = String.format("%s/%s/target/classes", baseDir, projectName); + if (new File(bundleDir).exists()) { return CoreOptions.bundle(String.format("reference:file:%s", bundleDir)); } - bundleDir = String.format("%s/../%s/target/classes",baseDir,projectName); - if(new File(bundleDir).exists()) { + bundleDir = String.format("%s/../%s/target/classes", baseDir, projectName); + if (new File(bundleDir).exists()) { return CoreOptions.bundle(String.format("reference:file:%s", bundleDir)); } return null; } - - public static MavenArtifactProvisionOption mavenBundleAsInProject(final String groupId,final String artifactId) { + + static MavenArtifactProvisionOption mavenBundleAsInProject(final String groupId, final String artifactId) { return CoreOptions.mavenBundle().groupId(groupId).artifactId(artifactId).versionAsInProject(); } - - public static Bundle findBundle(BundleContext context, String symbolicName) { + + static Bundle findBundle(BundleContext context, String symbolicName) { Bundle[] bundles = context.getBundles(); for (Bundle bundle : bundles) { if (bundle != null) { @@ -56,8 +43,7 @@ public static Bundle findBundle(BundleContext context, String symbolicName) { } } } - + return null; } } - diff --git a/affinity/pom.xml b/affinity/pom.xml index e85d99167..33f352b16 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -1,33 +1,22 @@ - - + + Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + +--> + + + 4.0.0 net.openhft java-parent-pom - 1.1.4 + 2026.0 - 4.0.0 affinity - 3.0.1-SNAPSHOT + 2026.3-SNAPSHOT bundle OpenHFT/Java-Thread-Affinity/affinity @@ -36,6 +25,8 @@ src/main/c UTF-8 + 0.40 + 0.4 @@ -43,8 +34,15 @@ net.openhft third-party-bom + 2026.0 + pom + import + + + net.openhft + chronicle-bom + 2026.0-SNAPSHOT pom - 3.4.23 import @@ -64,7 +62,7 @@ jna-platform - org.kohsuke.jetbrains + org.jetbrains annotations @@ -83,64 +81,78 @@ slf4j-simple test + + net.openhft + chronicle-test-framework + test + + + + slf4j-simple + + + org.slf4j + slf4j-simple + + + + + make-c + + + linux + !arm + + + !dontMake + + + + + + + org.codehaus.mojo + exec-maven-plugin + + + + build-native + process-classes + + exec + + + make + ${project.basedir}/${native.source.dir} + + + + + + + + + org.apache.maven.plugins maven-compiler-plugin - -Xlint:deprecation - 1.7 - 1.7 + + -h + ${project.build.directory}/jni + -Xlint:all,-options + + 1.8 + 1.8 UTF-8 - - - maven-antrun-plugin - 1.7 - - - build-native - process-classes - - run - - - - - - - - - - - - - - org.codehaus.mojo - exec-maven-plugin - 1.2.1 - - - - build-native - process-classes - - exec - - - ${project.basedir}/${native.source.dir}/Makefile - ${project.basedir}/${native.source.dir} - - - - - org.apache.maven.plugins maven-scm-publish-plugin @@ -179,7 +191,7 @@ maven-bundle-plugin true - + ${project.groupId}.${project.artifactId} OpenHFT :: ${project.artifactId} ${project.version} @@ -188,7 +200,8 @@ * - net.openhft.affinity.* + net.openhft.affinity.*;-noimport:=true, + net.openhft.ticker.*;-noimport:=true @@ -207,14 +220,21 @@ - - sonatype-nexus-staging - https://oss.sonatype.org/content/repositories/staging + + true + + chronicle-enterprise-snapshots + Snapshot Repository + https://nexus.chronicle.software/content/repositories/snapshots + + true + chronicle-enterprise-release + https://nexus.chronicle.software/content/repositories/releases @@ -223,7 +243,7 @@ scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git - master + ea diff --git a/affinity/src/main/adoc/requirements.adoc b/affinity/src/main/adoc/requirements.adoc new file mode 100644 index 000000000..8132561a5 --- /dev/null +++ b/affinity/src/main/adoc/requirements.adoc @@ -0,0 +1,304 @@ += Requirements Document: Java Thread Affinity +:toc: + +== 1. 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. +This allows developers to bind specific threads to designated CPU cores, which can lead to performance improvements, especially in latency-sensitive applications, by reducing context switching and improving cache utilisation. + +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 + +The scope of the Java Thread Affinity project includes: + +* Providing mechanisms to get and set thread affinity on supported operating systems. +* Offering a CPU locking mechanism (`AffinityLock`) to manage core reservations for threads or entire cores. +* Detecting or allowing specification of the CPU layout (sockets, cores, threads per core). +* Providing a high-resolution timer. +* Supporting inter-process lock checking for CPU core reservations. +* 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 + +CPU :: Central Processing Unit +JNA :: Java Native Access +JNI :: Java Native Interface +OS :: Operating System +PID :: Process Identifier +OSGi :: Open Service Gateway initiative +POM :: Project Object Model (Maven) +API :: Application Programming Interface + +== 4. References + +* Project Repository: link:https://github.com/OpenHFT/Java-Thread-Affinity[] +* JNA: link:https://github.com/java-native-access/jna[] + +== 5. 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 + +* 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 + +* _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 + +=== 6.1. 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)` +** `Affinity.setAffinity(int cpu)` +* *FR2*: The system _shall_ allow retrieving the current affinity mask of the current thread. +** `Affinity.getAffinity()` +* *FR3*: The system _shall_ allow querying the logical CPU ID the current thread is running on. +** `Affinity.getCpu()` +* *FR4*: The system _shall_ allow retrieving the native process ID of the current Java process. +** `IAffinity.getProcessId()` +* *FR5*: The system _shall_ allow retrieving the native thread ID of the current Java thread. +** `IAffinity.getThreadId()` +** `Affinity.setThreadId()` (to update `Thread.tid` via reflection if available) + +=== 6.2. 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()` +** `AffinityLock.acquireLock(boolean bind)` +** `AffinityLock.acquireLock(int cpuId)` +** `AffinityLock.acquireLock(String desc)` (e.g., "last", "last-N", "N", "any", "none", "csv:1,2,3") +* *FR6.2*: The system _shall_ provide a mechanism to acquire an exclusive lock on an entire physical core (including all its logical CPUs/hyper-threads). +** `AffinityLock.acquireCore()` +** `AffinityLock.acquireCore(boolean bind)` +* *FR6.3*: Acquired locks _shall_ be releasable, restoring the thread's affinity to a base state or a defined default. +** `AffinityLock.release()` +** `AffinityLock.close()` (for try-with-resources) +* *FR6.4*: The system _shall_ support affinity strategies for acquiring new locks relative to existing locks (e.g., same core, same socket, different core, different socket). +** `AffinityLock.acquireLock(AffinityStrategy... strategies)` +** `AffinityStrategies` enum: `ANY`, `SAME_CORE`, `SAME_SOCKET`, `DIFFERENT_CORE`, `DIFFERENT_SOCKET`. +* *FR6.5*: The system _shall_ provide a method to dump the current status of all CPU locks managed by the library. +** `AffinityLock.dumpLocks()` +* *FR6.6*: The system _shall_ allow querying if a lock is allocated and bound. +** `AffinityLock.isAllocated()` +** `AffinityLock.isBound()` + +=== 6.3. 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()` +* *FR7.2*: The system _shall_ allow applications to programmatically define the CPU layout. +** `AffinityLock.cpuLayout(CpuLayout cpuLayout)` +* *FR7.3*: The CPU layout _shall_ provide information about: +** Total number of logical CPUs: `CpuLayout.cpus()` +** Number of sockets: `CpuLayout.sockets()` +** Cores per socket: `CpuLayout.coresPerSocket()` +** Threads per core: `CpuLayout.threadsPerCore()` +** 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) + +* *FR8.1*: The system _shall_ provide a high-resolution time source. +** `Ticker.ticks()` (raw timer ticks) +** `Ticker.nanoTime()` (ticks converted to nanoseconds) +* *FR8.2*: If native JNI components are available and loaded (specifically `libCEInternals.so`), the timer _shall_ attempt to use `rdtsc` (Read Time-Stamp Counter). +** `JNIClock.rdtsc0()` +* *FR8.3*: If JNI-based `rdtsc` is not available, the timer _shall_ fall back to `System.nanoTime()`. +** `SystemClock.INSTANCE` +* *FR8.4*: The timer _shall_ provide methods to convert ticks to nanoseconds and microseconds. +** `ITicker.toNanos(long ticks)` +** `ITicker.toMicros(double ticks)` + +=== 6.5. 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`). +** *Windows*: Thread affinity control, Process ID, Thread ID via JNA (`WindowsJNAAffinity`). `getCpu()` returns -1. +** *macOS*: Process ID, Thread ID via JNA (`OSXJNAAffinity`). +No affinity modification; `getCpu()` returns -1. +** *Solaris*: Process ID, Thread ID via JNA (`SolarisJNAAffinity`). +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) + +* *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) + +* *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)` +* *FR11.2*: This mechanism _shall_ use file-based locks located in the directory specified by the `java.io.tmpdir` system property. +** `FileLockBasedLockChecker` +* *FR11.3*: The system _shall_ allow obtaining and releasing these inter-process locks for specified CPU IDs. +** `LockChecker.obtainLock(int id, int id2, String metaInfo)` +** `LockChecker.releaseLock(int id)` +* *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++) + +* *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.3*: The Java code _shall_ load this native library if available. +** `software.chronicle.enterprise.internals.impl.NativeAffinity.loadAffinityNativeLibrary()` + +== 7. Non-Functional Requirements + +* *NFR1. Platform Support*: +** *Primary Support*: Linux (full functionality). +** *Partial Support*: Windows (affinity setting, PID/TID, no `getCpu()`). +** *Limited Support*: macOS, Solaris (PID/TID only, no affinity setting or `getCpu()`). +* *NFR2. Dependencies*: +** *JNA*: `net.java.dev.jna:jna`, `net.java.dev.jna:jna-platform`. +Version 5.x or higher is recommended for full functionality. +The project currently uses version 4.4.0 (as per README, though POMs might show updates). +** *SLF4J API*: `org.slf4j:slf4j-api` for logging. +** *JetBrains Annotations*: `org.jetbrains:annotations` for code quality. +* *NFR3. Performance*: The library _should_ introduce minimal overhead. +Native calls _should_ be efficient. +The primary goal is to enable performance improvements in the client application. +* *NFR4. Licensing*: The project _shall_ be licensed under the Apache License, Version 2.0. +* *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++. +* *NFR7. Usability*: +** The API _should_ be clear and relatively simple to use. +** Javadoc _shall_ be provided for public APIs. +** Example usage _shall_ be available (e.g., in test sources and README). +* *NFR8. Error Handling and Resilience*: +** The library _shall_ degrade gracefully if JNA or native libraries are not available or if an OS does not support certain features (e.g., falling back to `NullAffinity`). +** Errors during native calls _should_ be appropriately logged and/or propagated as exceptions. +* *NFR9. Configuration*: +** Reserved CPUs for the application _shall_ be configurable via the system property `affinity.reserved={hex-mask}`. +** The lock file directory _shall_ default to `java.io.tmpdir` and be overridable by setting this system property. +* *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 + +=== 8.1. 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 + +* *`net.openhft.affinity.Affinity`*: Main public API facade for basic affinity operations. +* *`net.openhft.affinity.IAffinity`*: Interface defining the contract for OS-specific implementations. +** Concrete Implementations: `LinuxJNAAffinity`, `WindowsJNAAffinity`, `OSXJNAAffinity`, `SolarisJNAAffinity`, `PosixJNAAffinity`, `NativeAffinity` (JNI), `NullAffinity`. +* *`net.openhft.affinity.AffinityLock`*: Manages CPU reservations and bindings. +* *`net.openhft.affinity.LockInventory`*: Tracks the state of CPU locks based on `CpuLayout`. +* *`net.openhft.affinity.CpuLayout`*: Interface for CPU topology information. +** `VanillaCpuLayout`: Parses `/proc/cpuinfo` or properties files. +** `NoCpuLayout`: Default layout if detection fails. +* *`net.openhft.affinity.AffinityStrategies`*: Enum defining strategies for selecting CPUs. +* *`net.openhft.affinity.AffinityThreadFactory`*: A `java.util.concurrent.ThreadFactory` that sets affinity for new threads. +* *`net.openhft.ticker.Ticker`*: Provides high-resolution time. +** `JNIClock`: Uses `rdtsc` via JNI. +** `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`. + +=== 8.3. Maven Modules + +* *`Java-Thread-Affinity` (Parent POM)*: Aggregates sub-modules. +** Group ID: `net.openhft` +** Artifact ID: `Java-Thread-Affinity` +* *`affinity` (Core Library)*: Contains the main library code, JNA/JNI integrations, and native sources. +** Artifact ID: `affinity` +** Packaging: `bundle` (OSGi compatible) +* *`affinity-test` (Test Module)*: Contains OSGi integration tests and example usage. +** Artifact ID: `affinity-test` +** Packaging: `bundle` + +== 9. Native Components (libCEInternals.so) + +The library can utilise an optional native shared library, `libCEInternals.so`, for certain operations, primarily on Linux. + +* *Purpose*: Provides direct JNI implementations for thread affinity and the `rdtsc` timer. +* *Source Location*: `Java-Thread-Affinity/affinity/src/main/c/` +* *Build*: Compiled using the `Makefile` in the source directory (typically invoked by Maven's `exec-maven-plugin`). +* *Key Native Functions Implemented*: +** `Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getAffinity0` +** `Java_software_chronicle_enterprise_internals_impl_NativeAffinity_setAffinity0` +** `Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getCpu0` +** `Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getProcessId0` +** `Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getThreadId0` +** `Java_net_openhft_ticker_impl_JNIClock_rdtsc0` +* *Platform Specifics*: +** *Linux*: Uses `sched_getaffinity`, `sched_setaffinity`, `sched_getcpu`, `getpid`, `syscall(SYS_gettid)`. +** *macOS*: (Separate C file `software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c`) Uses `pthread_mach_thread_np`, `thread_policy_get`, `thread_policy_set`. +Note: JNA implementations are generally preferred on macOS. +* *Loading*: The `NativeAffinity.java` class attempts to load `System.loadLibrary("CEInternals")`. + +== 10. API Overview + +A brief overview of the primary public classes and interfaces: + +* *`net.openhft.affinity.Affinity`*: +** Static utility methods for basic affinity operations: `getAffinity()`, `setAffinity(BitSet)`, `setAffinity(int cpu)`, `getCpu()`, `getThreadId()`. +** Manages selection of the appropriate `IAffinity` implementation. +* *`net.openhft.affinity.AffinityLock`*: +** Manages acquisition and release of CPU locks: `acquireLock()`, `acquireCore()`, `release()`, `close()`. +** Configures CPU layout: `cpuLayout(CpuLayout)`. +** Provides information about reserved CPUs: `BASE_AFFINITY`, `RESERVED_AFFINITY`. +* *`net.openhft.affinity.AffinityStrategies`*: +** Enum defining CPU selection strategies for `AffinityLock`. +* *`net.openhft.affinity.CpuLayout`*: +** Interface to describe the machine's CPU topology. +* *`net.openhft.affinity.IAffinity`*: +** Core interface implemented by OS-specific providers. +* *`net.openhft.ticker.Ticker`*: +** Static utility for accessing high-resolution time: `ticks()`, `nanoTime()`. +* *`net.openhft.affinity.AffinityThreadFactory`*: +** Implements `java.util.concurrent.ThreadFactory` to create threads with specific affinity settings. + +== 11. Build and Deployment + +* The project is built using Apache Maven. +* The main artifact `net.openhft:affinity` is an OSGi bundle. +* Dependencies are managed via `pom.xml` files, including a `third-party-bom` and `chronicle-bom`. +* The `make-c` profile in `affinity/pom.xml` triggers the compilation of native C code using `make`. +* 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 + +The project includes a comprehensive suite of tests: + +* *Unit Tests*: Located in `affinity/src/test/java/`. +** `NativeAffinityTest`, `JnaAffinityTest`: Test core JNI/JNA functionalities. +** `AffinityLockTest`: Tests `AffinityLock` logic, including descriptions and inter-thread lock acquisition. +** `VanillaCpuLayoutTest`, `VanillaCpuLayoutPropertiesParseTest`: Test parsing of `cpuinfo` files and properties files for CPU layout. +** `TickerTest`, `JNIClockTest`: Test timer implementations. +** `LockCheckTest`, `FileLockLockCheckTest`: Test inter-process lock checking. +** `MultiProcessAffinityTest`: Tests affinity locking behavior across multiple Java processes. +* *OSGi Bundle Tests*: Located in `affinity-test/src/test/java/net/openhft/affinity/osgi/`. +** `OSGiBundleTest`: Verifies bundle activation and package exports in an OSGi environment using Pax Exam. +* *Test Resources*: Includes sample `cpuinfo` files for various architectures and corresponding properties files to test layout parsing. +** `affinity/src/test/resources/` +* *Test Infrastructure*: +** `BaseAffinityTest`: Provides common setup for tests, including temporary folder management for lock files. +** `chronicle-test-framework`: Used for some test utilities, like `JavaProcessBuilder` for multi-process tests. + +The tests cover various aspects including basic affinity setting, CPU layout parsing, lock management, multi-threading scenarios, multi-process contention, and OSGi integration. diff --git a/affinity/src/main/c/Makefile b/affinity/src/main/c/Makefile index 467161b89..ac448e568 100755 --- a/affinity/src/main/c/Makefile +++ b/affinity/src/main/c/Makefile @@ -1,4 +1,3 @@ -#!/usr/bin/make -f # # Makefile for C code # @@ -10,21 +9,29 @@ TARGET := $(TARGET_DIR)/libCEInternals.so WORKING_DIR := $(TARGET_DIR)/../jni +JNI_OS := win32 +UNAME_S:= $(shell uname -s) +ifeq ($(UNAME_S), Linux) + JNI_OS := linux + LRT := -lrt +endif +ifeq ($(UNAME_S), Darwin) + JNI_OS := darwin +endif + JAVA_CLASSES = software.chronicle.enterprise.internals.impl.NativeAffinity net.openhft.ticker.impl.JNIClock JNI_STUBS := $(subst .,_,$(JAVA_CLASSES)) -JNI_HEADERS := $(patsubst %,$(WORKING_DIR)/%.h,$(JNI_STUBS)) JNI_SOURCES := $(patsubst %,%.cpp,$(JNI_STUBS)) -JNI_JAVA_SOURCES := $(patsubst %,$(TARGET_DIR)/%.class,$(subst .,/,$(JAVA_CLASSES))) JAVA_BUILD_DIR := $(TARGET_DIR) JAVA_HOME ?= /usr/java/default JAVA_LIB := $(JAVA_HOME)/jre/lib -JVM_SHARED_LIBS := -L$(JAVA_LIB)/amd64/server -L$(JAVA_LIB)/i386/server -L$(JAVA_LIB)/amd64/jrockit/ -L$(JAVA_LIB)/i386/jrockit/ +JVM_SHARED_LIBS := -L"$(JAVA_LIB)/amd64/server" -L"$(JAVA_LIB)/i386/server" -L"$(JAVA_LIB)/amd64/jrockit" -L"$(JAVA_LIB)/i386/jrockit" -L"$(JAVA_LIB)/ppc64le/server" -L"$(JAVA_LIB)/ppc64le/jrockit" -L"$(JAVA_HOME)/lib/server" CXX=g++ -INCLUDES := -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/linux -I $(WORKING_DIR) +INCLUDES := -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/$(JNI_OS)" -I"$(WORKING_DIR)" # classpath for javah ifdef CLASSPATH @@ -37,12 +44,8 @@ endif all: $(TARGET) -$(TARGET): $(JNI_HEADERS) $(JNI_SOURCES) - $(CXX) -O3 -Wall -shared -fPIC $(JVM_SHARED_LIBS) -ljvm -lrt $(INCLUDES) $(JNI_SOURCES) -o $(TARGET) - -$(JNI_HEADERS): $(JNI_JAVA_SOURCES) - mkdir -p $(TARGET_DIR)/jni - javah -force -classpath $(JAVAH_CLASSPATH) -d $(WORKING_DIR) $(JAVA_CLASSES) +$(TARGET): $(JNI_SOURCES) + $(CXX) -O3 -Wall -shared -fPIC $(JVM_SHARED_LIBS) $(LRT) $(INCLUDES) $(JNI_SOURCES) -o $(TARGET) clean: rm $(TARGET) diff --git a/affinity/src/main/c/net_openhft_ticker_impl_JNIClock.cpp b/affinity/src/main/c/net_openhft_ticker_impl_JNIClock.cpp index a69ce129e..2cf233d3a 100644 --- a/affinity/src/main/c/net_openhft_ticker_impl_JNIClock.cpp +++ b/affinity/src/main/c/net_openhft_ticker_impl_JNIClock.cpp @@ -1,19 +1,6 @@ -/* vim: syntax=cpp - * Copyright 2015 Higher Frequency Trading - * - * 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. +/** + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif @@ -47,6 +34,36 @@ static __inline__ unsigned long long rdtsc (void) { clock_gettime (CLOCK_SGI_CYCLE, &tp); return (unsigned long long)(tp.tv_sec * (unsigned long long)1000000000) + (unsigned long long)tp.tv_nsec; } +#elif defined(__PPC64__) +unsigned long long rdtsc(){ + unsigned long long rval; + __asm__ __volatile__("mfspr %%r3, 268": "=r" (rval)); + return rval; +} +#elif defined(__aarch64__) // ARMv8-A (AArch64) +#include +inline uint64_t rdtsc() { + uint64_t virtual_timer_value; + asm volatile("mrs %0, cntvct_el0" : "=r"(virtual_timer_value)); + return virtual_timer_value; +} +#elif defined(__ARM_ARCH) && (__ARM_ARCH >= 7) // ARMv7-A (32-bit) +#include +inline uint64_t rdtsc() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec; +} +#elif defined(__APPLE__) +#include +inline uint64_t rdtsc() { + return mach_absolute_time(); +} +#elif defined(_MSC_VER) +#include +inline uint64_t rdtsc() { + return __rdtsc(); +} #endif /* diff --git a/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp index 0fad3536c..f13d566a1 100644 --- a/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp +++ b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp @@ -1,30 +1,19 @@ -/* vim: syntax=cpp - * Copyright 2011 Higher Frequency Trading - * - * 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. +/** + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #include -#include -#include -#include -#include -#include - +#ifdef __linux__ + #include + #include + #include + #include + #include +#endif +#include #include "software_chronicle_enterprise_internals_impl_NativeAffinity.h" /* @@ -35,6 +24,7 @@ JNIEXPORT jbyteArray JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getAffinity0 (JNIEnv *env, jclass c) { +#ifdef __linux__ // The default size of the structure supports 1024 CPUs, should be enough // for now In the future we can use dynamic sets, which can support more // CPUs, given OS can handle them as well @@ -53,6 +43,9 @@ JNIEXPORT jbyteArray JNICALL Java_software_chronicle_enterprise_internals_impl_N env->SetByteArrayRegion(ret, 0, size, bytes); return ret; +#else + throw std::runtime_error("Not supported"); +#endif } /* @@ -63,6 +56,7 @@ JNIEXPORT jbyteArray JNICALL Java_software_chronicle_enterprise_internals_impl_N JNIEXPORT void JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_setAffinity0 (JNIEnv *env, jclass c, jbyteArray affinity) { +#ifdef __linux__ cpu_set_t mask; const size_t size = sizeof(mask); CPU_ZERO(&mask); @@ -71,6 +65,9 @@ JNIEXPORT void JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA memcpy(&mask, bytes, size); sched_setaffinity(0, size, &mask); +#else + throw std::runtime_error("Not supported"); +#endif } /* @@ -80,7 +77,12 @@ JNIEXPORT void JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA */ JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getProcessId0 (JNIEnv *env, jclass c) { +#ifndef __linux__ + throw std::runtime_error("Not supported"); +#else + return (jint) getpid(); +#endif } /* @@ -90,7 +92,12 @@ JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA */ JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getThreadId0 (JNIEnv *env, jclass c) { +#ifndef __linux__ + throw std::runtime_error("Not supported"); +#else + return (jint) (pid_t) syscall (SYS_gettid); +#endif } /* @@ -100,8 +107,11 @@ JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeA */ JNIEXPORT jint JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getCpu0 (JNIEnv *env, jclass c) { +#ifndef __linux__ + throw std::runtime_error("Not supported"); +#else + return (jint) sched_getcpu(); +#endif } - - diff --git a/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c index a2d4666ce..67dafb1ef 100644 --- a/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c +++ b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c @@ -1,19 +1,6 @@ -/* - * Copyright 2011-2012 Peter Lawrey & Jerry Shea - * - * 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. +/** + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - #include #include #include diff --git a/affinity/src/main/java/java/lang/ThreadLifecycleListener.java b/affinity/src/main/java/java/lang/ThreadLifecycleListener.java deleted file mode 100644 index 5df73921a..000000000 --- a/affinity/src/main/java/java/lang/ThreadLifecycleListener.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package java.lang; - -/** - * A listener for various events in a Thread's life: creation, termination, etc. - */ -public interface ThreadLifecycleListener { - - /** - * The specified thread is about to be started. - * @param t the thread which is being started - */ - void started(Thread t); - - /** - * The specified thread failed to start. - * @param t the thread that had a failed start - */ - void startFailed(Thread t); - - /** - * The specified thread has been terminated. - * @param t the thread that has been terminated - */ - void terminated(Thread t); -} diff --git a/affinity/src/main/java/java/lang/ThreadTrackingGroup.java b/affinity/src/main/java/java/lang/ThreadTrackingGroup.java deleted file mode 100644 index ad1ce10e8..000000000 --- a/affinity/src/main/java/java/lang/ThreadTrackingGroup.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package java.lang; - -/** - * A wrapper of {@link java.lang.ThreadGroup} that tracks the creation and termination of threads. - */ -public class ThreadTrackingGroup extends ThreadGroup { - - /** - * Listener to be notified of various events in thread lifecycles. - */ - private final ThreadLifecycleListener listener; - - public ThreadTrackingGroup(ThreadGroup parent, ThreadLifecycleListener listener) { - super(parent, ThreadTrackingGroup.class.getSimpleName().toLowerCase() + System.identityHashCode(listener)); - this.listener = listener; - } - - @Override - void add(Thread t) { - // System.out.println("ThreadTrackingGroup.add: " + t); //todo: remove - super.add(t); - listener.started(t); - } - - @Override - void threadStartFailed(Thread t) { - super.threadStartFailed(t); - listener.startFailed(t); - } - - @Override - void threadTerminated(Thread t) { - super.threadTerminated(t); - listener.terminated(t); - } -} diff --git a/affinity/src/main/java/net/openhft/affinity/Affinity.java b/affinity/src/main/java/net/openhft/affinity/Affinity.java index be8aa02bf..8dadd8a15 100644 --- a/affinity/src/main/java/net/openhft/affinity/Affinity.java +++ b/affinity/src/main/java/net/openhft/affinity/Affinity.java @@ -1,21 +1,9 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; +import com.sun.jna.Native; import net.openhft.affinity.impl.*; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -27,12 +15,13 @@ import java.util.BitSet; /** - * Library to wrap low level JNI or JNA calls. Can be called without needing to know the actual implementation used. + * Library to wrap low level JNI or JNA calls. Can be called without needing to know the actual + * implementation used. * * @author peter.lawrey */ public enum Affinity { - ; + ; // none static final Logger LOGGER = LoggerFactory.getLogger(Affinity.class); @NotNull private static final IAffinity AFFINITY_IMPL; @@ -126,7 +115,7 @@ private static boolean isMacJNAAffinityUsable() { return true; } else { - LOGGER.warn("MAX OSX JNA-based affinity not usable due to JNA not being available!"); + LOGGER.warn("MAC OSX JNA-based affinity not usable due to JNA not being available!"); return false; } } @@ -153,16 +142,16 @@ public static BitSet getAffinity() { return AFFINITY_IMPL.getAffinity(); } + public static void setAffinity(final BitSet affinity) { + AFFINITY_IMPL.setAffinity(affinity); + } + public static void setAffinity(int cpu) { BitSet affinity = new BitSet(Runtime.getRuntime().availableProcessors()); affinity.set(cpu); setAffinity(affinity); } - public static void setAffinity(final BitSet affinity) { - AFFINITY_IMPL.setAffinity(affinity); - } - public static int getCpu() { return AFFINITY_IMPL.getCpu(); } @@ -185,34 +174,40 @@ public static void setThreadId() { } public static boolean isJNAAvailable() { - if (JNAAvailable == null) - try { - Class.forName("com.sun.jna.Platform"); - JNAAvailable = true; - } catch (ClassNotFoundException ignored) { + if (JNAAvailable == null) { + int majorVersion = Integer.parseInt(Native.VERSION.split("\\.")[0]); + if (majorVersion < 5) { + LOGGER.warn("Affinity library requires JNA version >= 5"); JNAAvailable = false; + } else { + try { + Class.forName("com.sun.jna.Platform"); + JNAAvailable = true; + } catch (ClassNotFoundException ignored) { + JNAAvailable = false; + } } + } return JNAAvailable; } public static AffinityLock acquireLock() { - return isNonForkingAffinityAvailable() ? NonForkingAffinityLock.acquireLock() : AffinityLock.acquireLock(); + return AffinityLock.acquireLock(); } public static AffinityLock acquireCore() { - return isNonForkingAffinityAvailable() ? NonForkingAffinityLock.acquireCore() : AffinityLock.acquireCore(); + return AffinityLock.acquireCore(); } public static AffinityLock acquireLock(boolean bind) { - return isNonForkingAffinityAvailable() ? NonForkingAffinityLock.acquireLock(bind) : AffinityLock.acquireLock(bind); + return AffinityLock.acquireLock(bind); } public static AffinityLock acquireCore(boolean bind) { - return isNonForkingAffinityAvailable() ? NonForkingAffinityLock.acquireCore(bind) : AffinityLock.acquireCore(bind); + return AffinityLock.acquireCore(bind); } - private static boolean isNonForkingAffinityAvailable() { - BootClassPath bootClassPath = BootClassPath.INSTANCE; - return bootClassPath.has("java.lang.ThreadTrackingGroup") && bootClassPath.has("java.lang.ThreadLifecycleListener"); + public static void resetToBaseAffinity() { + 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 73c33617e..cf0ed4ca0 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityLock.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityLock.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; import net.openhft.affinity.impl.NoCpuLayout; @@ -25,40 +12,50 @@ import java.io.Closeable; import java.io.File; -import java.io.IOException; +import java.util.Arrays; import java.util.BitSet; /** - * This utility class support locking a thread to a single core, or reserving a whole core for a thread. + * This utility class support locking a thread to a single core, or reserving a whole core for a + * thread. * * @author peter.lawrey */ public class AffinityLock implements Closeable { - private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLock.class); - // Static fields and methods. public static final String AFFINITY_RESERVED = "affinity.reserved"; - // TODO It seems like on virtualized platforms .availableProcessors() value can change at // TODO runtime. We should think about how to adopt to such change - public static final int PROCESSORS = Runtime.getRuntime().availableProcessors(); - public static final BitSet BASE_AFFINITY = Affinity.getAffinity(); - public static final BitSet RESERVED_AFFINITY = getReservedAffinity0(); - private static final LockInventory LOCK_INVENTORY = new LockInventory(new NoCpuLayout(PROCESSORS)); + public static final int PROCESSORS; + + public static final BitSet BASE_AFFINITY; + public static final BitSet RESERVED_AFFINITY; + static final int ANY_CPU = -1; + private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLock.class); + private static final LockInventory LOCK_INVENTORY; static { + int processors = Runtime.getRuntime().availableProcessors(); + VanillaCpuLayout cpuLayout = null; try { if (new File("/proc/cpuinfo").exists()) { - cpuLayout(VanillaCpuLayout.fromCpuInfo()); + cpuLayout = VanillaCpuLayout.fromCpuInfo(); + processors = cpuLayout.cpus(); } - } catch (IOException e) { + } catch (Throwable e) { LOGGER.warn("Unable to load /proc/cpuinfo", e); } + PROCESSORS = processors; + BASE_AFFINITY = Affinity.getAffinity(); + RESERVED_AFFINITY = getReservedAffinity0(); + LOCK_INVENTORY = new LockInventory(cpuLayout == null ? new NoCpuLayout(PROCESSORS) : cpuLayout); } + /** * Logical ID of the CPU to which this lock belongs to. */ private final int cpuId; + private final int cpuId2; /** * CPU to which this lock belongs to is of general use. */ @@ -68,26 +65,28 @@ public class AffinityLock implements Closeable { */ private final boolean reservable; /** - * An inventory build from the CPU layout which keeps track of the various locks - * belonging to each CPU. + * An inventory build from the CPU layout which keeps track of the various locks belonging to + * each CPU. */ private final LockInventory lockInventory; boolean bound = false; @Nullable Thread assignedThread; + Throwable boundHere; + private boolean resetAffinity = true; - AffinityLock(int cpuId, boolean base, boolean reservable, LockInventory lockInventory) { + AffinityLock(int cpuId, int cpuId2, boolean base, boolean reservable, LockInventory lockInventory) { this.lockInventory = lockInventory; this.cpuId = cpuId; + this.cpuId2 = cpuId2; this.base = base; this.reservable = reservable; } /** - * Set the CPU layout for this machine. CPUs which are not mentioned will be ignored. - *

- * Changing the layout will have no impact on thread which have already been assigned. - * It only affects subsequent assignments. + * Set the CPU layout for this machine. CPUs which are not mentioned will be ignored.

+ * Changing the layout will have no impact on thread which have already been assigned. It only + * affects subsequent assignments. * * @param cpuLayout for this application to use for this machine. */ @@ -103,28 +102,28 @@ public static CpuLayout cpuLayout() { return LOCK_INVENTORY.getCpuLayout(); } - private static BitSet getReservedAffinity0() - { + private static BitSet getReservedAffinity0() { String reservedAffinity = System.getProperty(AFFINITY_RESERVED); - if (reservedAffinity == null || reservedAffinity.trim().isEmpty()) - { + if (BASE_AFFINITY != null && (reservedAffinity == null || reservedAffinity.trim().isEmpty())) { BitSet reserverable = new BitSet(PROCESSORS); - reserverable.set(0, PROCESSORS - 1, true); - reserverable.and(BASE_AFFINITY); - if (reserverable.isEmpty() && PROCESSORS > 1) - { - LoggerFactory.getLogger(AffinityLock.class).info("No isolated CPUs found, so assuming CPUs 1 to {} available.", (PROCESSORS - 1)); - reserverable = new BitSet(PROCESSORS); - // make the first CPU unavailable - reserverable.set(1, PROCESSORS - 1, true); - reserverable.set(0, false); + reserverable.set(1, PROCESSORS, true); + reserverable.andNot(BASE_AFFINITY); + if (reserverable.isEmpty() && PROCESSORS > 1) { + // make all but first CPUs available + reserverable.set(1, PROCESSORS); return reserverable; } return reserverable; } - long[] longs = new long[1]; - longs[0] = Long.parseLong(reservedAffinity, 16); + reservedAffinity = reservedAffinity.trim(); + long[] longs = new long[1 + (reservedAffinity.length() - 1) / 16]; + int end = reservedAffinity.length(); + for (int i = 0; i < longs.length; i++) { + int begin = Math.max(0, end - 16); + longs[i] = Long.parseUnsignedLong(reservedAffinity.substring(begin, end), 16); + end = begin; + } return BitSet.valueOf(longs); } @@ -137,10 +136,17 @@ public static AffinityLock acquireLock() { return acquireLock(true); } + 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)); + } + } + } + /** - * Assign any free core to this thread. - *

- * In reality, only one cpu is assigned, the rest of the threads for that core are reservable so they are not used. + * Assign any free core to this thread.

In reality, only one cpu is assigned, the rest of + * the threads for that core are reservable so they are not used. * * @return A handle for the current AffinityLock. */ @@ -149,34 +155,167 @@ public static AffinityLock acquireCore() { } /** - * Assign a cpu which can be bound to the current thread or another thread. - *

- * This can be used for defining your thread layout centrally and passing the handle via dependency injection. + * Assign a cpu which can be bound to the current thread or another thread.

This can be used + * for defining your thread layout centrally and passing the handle via dependency injection. * - * @param bind if true, bind the current thread, if false, reserve a cpu which can be bound later. + * @param bind if true, bind the current thread, if false, reserve a cpu which can be bound + * later. * @return A handle for an affinity lock. */ public static AffinityLock acquireLock(boolean bind) { - return acquireLock(bind, -1, AffinityStrategies.ANY); + return acquireLock(bind, ANY_CPU, AffinityStrategies.ANY); + } + + /** + * Assign a cpu which can be bound to the current thread or another thread.

This can be used + * for defining your thread layout centrally and passing the handle via dependency injection. + * + * @param cpuId the CPU id to bind to + * @return A handle for an affinity lock, or no lock if no available CPU in the array + */ + public static AffinityLock acquireLock(int cpuId) { + if (isInvalidCpuId(cpuId)) + return LOCK_INVENTORY.noLock(); + return acquireLock(true, cpuId, AffinityStrategies.ANY); + } + + private static boolean isInvalidCpuId(int cpuId) { + if (cpuId < 0 || cpuId >= PROCESSORS) { + LOGGER.warn("cpuId must be between 0 and {}: {}", PROCESSORS - 1, cpuId); + return true; + } + return false; + } + + /** + * Assign a cpu which can be bound to the current thread or another thread + * Caller passes in an explicit set of preferred CPUs + * The first available CPU is used, and the lock returned + * If all CPUs in the set are unavailable then no lock is obtained + * + * @param cpus the array of available CPUs to bind to + * @return A handle for an affinity lock, or nolock if no available CPU in the array + */ + public static AffinityLock acquireLock(int[] cpus) { + for (int cpu : cpus) { + if (isInvalidCpuId(cpu)) continue; + AffinityLock lock = tryAcquireLock(true, cpu); + if (lock != null) { + LOGGER.info("Acquired lock on CPU {}", cpu); + return lock; + } + } + + LOGGER.warn("Failed to lock any CPU in explicit list {}", Arrays.toString(cpus)); + return LOCK_INVENTORY.noLock(); + } + + /** + * Allocate from the end. + * + * @param n positive number to allocate from. + * @return the lock acquired + */ + public static AffinityLock acquireLockLastMinus(int n) { + return acquireLock(true, PROCESSORS - n, AffinityStrategies.ANY); + } + + /** + * Use a description to allocate a cpu. + *

    + *
  • "N" being a positive integer means allocate this CPU,
  • + *
  • "last" or "last-N" means allocate from the end,
  • + *
  • "csv:1,2,5,6" eg means allocate first free core from the provided
  • + *
  • "any" means allow any
  • + *
  • "none" or null means
  • + *
  • "0" is not allowed
  • + *
+ * + * @param desc of which cpu to pick + * @return the AffinityLock obtained + */ + public static AffinityLock acquireLock(String desc) { + if (desc == null) + return LOCK_INVENTORY.noLock(); + + desc = desc.toLowerCase(); + int cpuId; + if (desc.startsWith("last")) { + String last = desc.substring(4); + int lastN; + if (last.isEmpty()) + lastN = 0; + else + try { + lastN = Integer.parseInt(last); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Cannot parse '" + desc + "'", e); + } + if (lastN > 0) + throw new IllegalArgumentException("Cannot parse '" + desc + "'"); + + cpuId = PROCESSORS + lastN - 1; + + } else if (desc.startsWith("csv:")) { + String content = desc.substring(4); + int[] cpus = Arrays.stream(content.split(",")) + .map(String::trim) + .mapToInt(Integer::parseInt).toArray(); + + return acquireLock(cpus); + + } else if (desc.equals("none")) { + return LOCK_INVENTORY.noLock(); + + } else if (desc.equals("any")) { + return acquireLock(); + + } else { + try { + cpuId = Integer.parseInt(desc); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Cannot parse '" + desc + "'", e); + } + } + if (cpuId <= 0) { + LOGGER.warn("Cannot allocate 0 or negative cpuIds '{}'", desc); + return LOCK_INVENTORY.noLock(); + } + return acquireLock(cpuId); } /** * Assign a core(and all its cpus) which can be bound to the current thread or another thread. - *

- * This can be used for defining your thread layout centrally and passing the handle via dependency injection. + *

This can be used for defining your thread layout centrally and passing the handle via + * dependency injection. * - * @param bind if true, bind the current thread, if false, reserve a cpu which can be bound later. + * @param bind if true, bind the current thread, if false, reserve a cpu which can be bound + * later. * @return A handle for an affinity lock. */ public static AffinityLock acquireCore(boolean bind) { - return acquireCore(bind, -1, AffinityStrategies.ANY); + return acquireCore(bind, ANY_CPU, AffinityStrategies.ANY); } private static AffinityLock acquireLock(boolean bind, int cpuId, @NotNull AffinityStrategy... strategies) { + Warnings.warmNoReservedCPUs(); return LOCK_INVENTORY.acquireLock(bind, cpuId, strategies); } + /** + * Try to acquire a lock on the specified core + * Returns lock if successful, or null if cpu cannot be acquired + * + * @param bind - if true, bind the current thread; if false, reserve a cpu which can be bound later + * @param cpuId - the cpu to lock + * @return - A handle to an affinity lock on success; null if failed to lock + */ + private static AffinityLock tryAcquireLock(boolean bind, int cpuId) { + return LOCK_INVENTORY.tryAcquireLock(bind, cpuId); + } + private static AffinityLock acquireCore(boolean bind, int cpuId, @NotNull AffinityStrategy... strategies) { + Warnings.warmNoReservedCPUs(); return LOCK_INVENTORY.acquireCore(bind, cpuId, strategies); } @@ -188,8 +327,33 @@ public static String dumpLocks() { return LOCK_INVENTORY.dumpLocks(); } + private static boolean areAssertionsEnabled() { + boolean debug = false; + //noinspection AssertWithSideEffects + assert debug = true; + //noinspection ConstantValue + return debug; + } + /** - * Assigning the current thread has a side effect of preventing the lock being used again until it is released. + * @return Whether to reset the affinity, false indicates the thread is about to die anyway. + */ + public boolean resetAffinity() { + return resetAffinity; + } + + /** + * @param resetAffinity Whether to reset the affinity, false indicates the thread is about to die anyway. + * @return this + */ + public AffinityLock resetAffinity(boolean resetAffinity) { + this.resetAffinity = resetAffinity; + return this; + } + + /** + * Assigning the current thread has a side effect of preventing the lock being used again until + * it is released. * * @param bind whether to bind the thread as well * @param wholeCore whether to reserve all the thread in the same core. @@ -216,24 +380,31 @@ public void bind(boolean wholeCore) { if (bound && assignedThread != null && assignedThread.isAlive()) throw new IllegalStateException("cpu " + cpuId + " already bound to " + assignedThread); + if (areAssertionsEnabled()) + boundHere = new Throwable("Bound here"); if (wholeCore) { lockInventory.bindWholeCore(cpuId); } else if (cpuId >= 0) { bound = true; assignedThread = Thread.currentThread(); - LOGGER.info("Assigning cpu {} to {}", cpuId, assignedThread); + LOGGER.info("Assigning cpu {} to {} on thread id {}", cpuId, assignedThread, Affinity.getThreadId()); } - if (cpuId >= 0) - { + if (cpuId >= 0) { BitSet affinity = new BitSet(); affinity.set(cpuId, true); Affinity.setAffinity(affinity); } } - final boolean canReserve() { - if (!reservable) return false; + final boolean canReserve(boolean specified) { + + if (!specified && !reservable) + return false; + + if (!LockCheck.isCpuFree(cpuId)) + return false; + if (assignedThread != null) { if (assignedThread.isAlive()) { return false; @@ -245,10 +416,10 @@ final boolean canReserve() { } /** - * Give another affinity lock relative to this one based on a list of strategies. - *

- * The strategies are evaluated in order to (like a search path) to find the next appropriate thread. - * If ANY is not the last strategy, a warning is logged and no cpu is assigned (leaving the OS to choose) + * Give another affinity lock relative to this one based on a list of strategies.

The + * strategies are evaluated in order to (like a search path) to find the next appropriate + * thread. If ANY is not the last strategy, a warning is logged and no cpu is assigned (leaving + * the OS to choose) * * @param strategies To determine if you want the same/different core/socket. * @return A matching AffinityLock. @@ -261,7 +432,12 @@ public AffinityLock acquireLock(AffinityStrategy... strategies) { * Release the current AffinityLock which can be discarded. */ public void release() { - lockInventory.release(); + if (cpuId == ANY_CPU) + return; + // expensive if not actually used. + boolean resetAffinity = this.resetAffinity; + this.resetAffinity = true; + lockInventory.release(resetAffinity); } @Override @@ -269,10 +445,11 @@ public void close() { release(); } + @SuppressWarnings({"deprecation", "removal"}) @Override protected void finalize() throws Throwable { - if (reservable) { - LOGGER.warn("Affinity lock for {} was discarded rather than release()d in a controlled manner.", assignedThread); + if (bound) { + LOGGER.warn("Affinity lock for {} was discarded rather than release()d in a controlled manner.", assignedThread, boundHere); release(); } super.finalize(); @@ -285,6 +462,10 @@ public int cpuId() { return cpuId; } + public int cpuId2() { + return cpuId2; + } + /** * @return Was a cpu found to bind this lock to. */ @@ -312,8 +493,4 @@ else if (base) sb.append("CPU not available"); return sb.toString(); } - - public static void main(String[] args) { - System.out.println("Test"); - } } diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityStrategies.java b/affinity/src/main/java/net/openhft/affinity/AffinityStrategies.java index 71b498c42..65ee0cbf9 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityStrategies.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityStrategies.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; /** @@ -22,6 +9,7 @@ * @author peter.lawrey */ public enum AffinityStrategies implements AffinityStrategy { + /** * Any free cpu. */ diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityStrategy.java b/affinity/src/main/java/net/openhft/affinity/AffinityStrategy.java index 4fe63a4e3..400c6d3c6 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityStrategy.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityStrategy.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; /** diff --git a/affinity/src/main/java/net/openhft/affinity/AffinitySupport.java b/affinity/src/main/java/net/openhft/affinity/AffinitySupport.java deleted file mode 100644 index c3a32af32..000000000 --- a/affinity/src/main/java/net/openhft/affinity/AffinitySupport.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package net.openhft.affinity; - -/** - * For backward compatibility with Affinity 2.x - */ -@Deprecated -public class AffinitySupport { - - public static int getThreadId() { - return Affinity.getThreadId(); - } - public static void setThreadId() { - Affinity.setThreadId(); - } - -} diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java index 70c396617..6e1d67e5a 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; import org.jetbrains.annotations.NotNull; @@ -52,20 +39,22 @@ public AffinityThreadFactory(String name, boolean daemon, @NotNull AffinityStrat public synchronized Thread newThread(@NotNull final Runnable r) { String name2 = id <= 1 ? name : (name + '-' + id); id++; - Thread t = new Thread(new Runnable() { - @Override - public void run() { - AffinityLock al = lastAffinityLock == null ? AffinityLock.acquireLock() : lastAffinityLock.acquireLock(strategies); - try { - if (al.cpuId() >= 0) - lastAffinityLock = al; - r.run(); - } finally { - al.release(); - } + Thread t = new Thread(() -> { + try (AffinityLock ignored = acquireLockBasedOnLast()) { + //noinspection ConstantValue + assert ignored != null; + r.run(); } }, name2); t.setDaemon(daemon); return t; } + + private synchronized AffinityLock acquireLockBasedOnLast() { + AffinityLock al = lastAffinityLock == null ? AffinityLock.acquireLock(false) : lastAffinityLock.acquireLock(strategies); + al.bind(); + if (al.cpuId() >= 0) + lastAffinityLock = al; + return al; + } } diff --git a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java index d3ebbf6b0..85efa3279 100644 --- a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java +++ b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java @@ -1,58 +1,132 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sun.misc.URLClassPath; import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; +import java.io.IOException; +import java.net.URI; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; enum BootClassPath { INSTANCE; - private final URLClassPath bootClassPath = new URLClassPath(getBootClassPathURLs()); + private final Set bootClassPathResources = Collections.unmodifiableSet(getResourcesOnBootClasspath()); - public final boolean has(String binaryClassName) { - String resourceClassName = binaryClassName.replace('.', '/').concat(".class"); - return bootClassPath.getResource(resourceClassName, false) != null; + private static Set getResourcesOnBootClasspath() { + final Logger logger = LoggerFactory.getLogger(BootClassPath.class); + final Set resources = new HashSet<>(); + + final String bootClassPath = System.getProperty("sun.boot.class.path", ""); + if (!bootClassPath.isEmpty()) { + logger.trace("Boot class-path is: {}", bootClassPath); + + final String pathSeparator = File.pathSeparator; + logger.trace("Path separator is: '{}'", pathSeparator); + + final String[] pathElements = bootClassPath.split(pathSeparator); + + for (final String pathElement : pathElements) { + resources.addAll(findResources(Paths.get(pathElement), logger)); + } + } else { + resources.addAll(findResourcesInJrt(logger)); + } + + return resources; } - private URL[] getBootClassPathURLs() { - Logger LOGGER = LoggerFactory.getLogger(BootClassPath.class); + private static Set findResourcesInJrt(final Logger logger) { + final Set jrtResources = new HashSet<>(); try { - String bootClassPath = System.getProperty("sun.boot.class.path"); - LOGGER.trace("Boot class-path is: {}",bootClassPath); + FileSystem fs; + try { + fs = FileSystems.getFileSystem(URI.create("jrt:/")); + } catch (FileSystemNotFoundException | ProviderNotFoundException e) { + fs = FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap()); + } + final Path modules = fs.getPath("/modules"); + Files.walkFileTree(modules, new SimpleFileVisitor() { + @Override + public @NotNull FileVisitResult visitFile(final @NotNull Path file, + final @NotNull BasicFileAttributes attrs) throws IOException { + if (file.getFileName().toString().endsWith(".class")) { + Path relative = modules.relativize(file); + if (relative.getNameCount() > 1) { + Path classPath = relative.subpath(1, relative.getNameCount()); + jrtResources.add(classPath.toString()); + } + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + logger.warn("Error walking jrt filesystem", e); + } + return jrtResources; + } - String pathSeparator = System.getProperty("path.separator"); - LOGGER.trace("Path separator is: '{}'", pathSeparator); + private static Set findResources(final Path path, final Logger logger) { + if (!Files.exists(path)) { + return Collections.emptySet(); + } - String[] pathElements = bootClassPath.split(pathSeparator); - URL[] pathURLs = new URL[pathElements.length]; - for (int i = 0; i < pathElements.length; i++) { - pathURLs[i] = new File(pathElements[i]).toURI().toURL(); + if (Files.isDirectory(path)) { + return findResourcesInDirectory(path, logger); + } + + return findResourcesInJar(path, logger); + } + + private static Set findResourcesInJar(final Path path, final Logger logger) { + final Set jarResources = new HashSet<>(); + try (final JarFile jarFile = new JarFile(path.toFile())) { + final Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + final JarEntry jarEntry = entries.nextElement(); + if (jarEntry.getName().endsWith(".class")) { + jarResources.add(jarEntry.getName()); + } } + } catch (IOException e) { + logger.warn("Not a jar file: {}", path); + } + + return jarResources; + } - return pathURLs; - } catch (MalformedURLException e) { - LOGGER.warn("Parsing the boot class-path failed! Reason: {}", e.getMessage()); - return new URL[0]; + private static Set findResourcesInDirectory(final Path path, final Logger logger) { + final Set dirResources = new HashSet<>(); + try { + Files.walkFileTree(path, new SimpleFileVisitor() { + @Override + public @NotNull FileVisitResult visitFile(final @NotNull Path file, final @NotNull BasicFileAttributes attrs) throws IOException { + if (file.getFileName().toString().endsWith(".class")) { + dirResources.add(path.relativize(file).toString()); + } + return super.visitFile(file, attrs); + } + }); + } catch (IOException e) { + logger.warn("Error walking dir: {}", path, e); } + + return dirResources; + } + + public final 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/CpuLayout.java b/affinity/src/main/java/net/openhft/affinity/CpuLayout.java index 8293cc03a..0cbdd6412 100644 --- a/affinity/src/main/java/net/openhft/affinity/CpuLayout.java +++ b/affinity/src/main/java/net/openhft/affinity/CpuLayout.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; /** @@ -48,4 +35,10 @@ public interface CpuLayout { * @return which thread on a core this cpu is on. */ int threadId(int cpuId); + + /** + * @param cpuId the logical processor number + * @return the hyperthreaded pair number or 0 if not hyperthreaded. + */ + int pair(int cpuId); } diff --git a/affinity/src/main/java/net/openhft/affinity/IAffinity.java b/affinity/src/main/java/net/openhft/affinity/IAffinity.java index 672e895d7..c473a5400 100644 --- a/affinity/src/main/java/net/openhft/affinity/IAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/IAffinity.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; import java.util.BitSet; diff --git a/affinity/src/main/java/net/openhft/affinity/LockCheck.java b/affinity/src/main/java/net/openhft/affinity/LockCheck.java new file mode 100644 index 000000000..e3c485fed --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/LockCheck.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import net.openhft.affinity.impl.Utilities; +import net.openhft.affinity.lockchecker.FileLockBasedLockChecker; +import net.openhft.affinity.lockchecker.LockChecker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +/** + * @author Rob Austin. + */ +public enum LockCheck { + ; // none + + private static final Logger LOGGER = LoggerFactory.getLogger(LockCheck.class); + private static final String OS = System.getProperty("os.name").toLowerCase(); + static final boolean IS_LINUX = OS.startsWith("linux"); + private static final int EMPTY_PID = Integer.MIN_VALUE; + + private static final LockChecker lockChecker = FileLockBasedLockChecker.getInstance(); + + public static long getPID() { + return Utilities.currentProcessId(); + } + + static boolean canOSSupportOperation() { + return IS_LINUX; + } + + public static boolean isCpuFree(int cpu) { + if (!canOSSupportOperation()) + return true; + + return isLockFree(cpu); + } + + static boolean replacePid(int cpu, int cpu2, long processID) throws IOException { + return storePid(processID, cpu, cpu2); + } + + public static boolean isProcessRunning(long pid) { + if (canOSSupportOperation()) + return new File("/proc/" + pid).exists(); + else + throw new UnsupportedOperationException("this is only supported on LINUX"); + } + + /** + * stores the pid in a file, named by the core, the pid is written to the file with the date + * below + */ + private synchronized static boolean storePid(long processID, int cpu, int cpu2) throws IOException { + return lockChecker.obtainLock(cpu, cpu2, Long.toString(processID)); + } + + private synchronized static boolean isLockFree(int id) { + return lockChecker.isLockFree(id); + } + + public static int getProcessForCpu(int core) throws IOException { + if (!canOSSupportOperation()) + return EMPTY_PID; + + String meta = lockChecker.getMetaInfo(core); + + if (meta != null && !meta.isEmpty()) { + try { + return Integer.parseInt(meta); + } catch (NumberFormatException e) { + //nothing + } + } + return EMPTY_PID; + } + + static boolean updateCpu(int cpu, int cpu2) throws IOException { + if (!canOSSupportOperation()) + return true; + return replacePid(cpu, cpu2, getPID()); + } + + public static void releaseLock(int cpu) { + lockChecker.releaseLock(cpu); + } +} diff --git a/affinity/src/main/java/net/openhft/affinity/LockInventory.java b/affinity/src/main/java/net/openhft/affinity/LockInventory.java index fb6ae4467..b54615219 100644 --- a/affinity/src/main/java/net/openhft/affinity/LockInventory.java +++ b/affinity/src/main/java/net/openhft/affinity/LockInventory.java @@ -1,28 +1,21 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; +import net.openhft.affinity.impl.NullAffinity; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; +import java.nio.channels.ClosedByInterruptException; +import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; +import static net.openhft.affinity.Affinity.getAffinityImpl; + class LockInventory { private static final Logger LOGGER = LoggerFactory.getLogger(LockInventory.class); @@ -30,7 +23,7 @@ class LockInventory { * The locks belonging to physical cores. Since a physical core can host multiple logical cores * the relationship is one to many. */ - private final NavigableMap physicalCoreLocks = new TreeMap(); + private final NavigableMap physicalCoreLocks = new TreeMap<>(); private CpuLayout cpuLayout; /** * The lock belonging to each logical core. 1-to-1 relationship @@ -46,12 +39,48 @@ public static String dumpLocks(@NotNull AffinityLock[] locks) { for (int i = 0; i < locks.length; i++) { AffinityLock al = locks[i]; sb.append(i).append(": "); - sb.append(al.toString()); + sb.append(al); sb.append('\n'); } return sb.toString(); } + private static boolean anyStrategyMatches(final int cpuOne, final int cpuTwo, final AffinityStrategy[] strategies) { + for (AffinityStrategy strategy : strategies) { + if (strategy.matches(cpuOne, cpuTwo)) { + return true; + } + } + return false; + } + + private static boolean isAnyCpu(final int cpuId) { + return cpuId == AffinityLock.ANY_CPU; + } + + /** + * Update the lock for the current thread + * + * @param bind Whether to also bind the thread to the core + * @param al The lock to update + * @param wholeCore Whether to bind the whole core + * @return true if the lock was acquired, false otherwise + */ + private static boolean updateLockForCurrentThread(final boolean bind, final AffinityLock al, final boolean wholeCore) throws ClosedByInterruptException { + try { + if (LockCheck.updateCpu(al.cpuId(), wholeCore ? al.cpuId2() : 0)) { + al.assignCurrentThread(bind, wholeCore); + return true; + } + } catch (ClosedByInterruptException e) { + throw e; + + } catch (IOException e) { + LOGGER.info("Error occurred acquiring lock, trying another {}", String.valueOf(e)); + } + return false; + } + public final synchronized CpuLayout getCpuLayout() { return cpuLayout; } @@ -61,13 +90,13 @@ public final synchronized void set(CpuLayout cpuLayout) { return; } reset(cpuLayout); - for (int i = 0; i < cpuLayout.cpus(); i++) - { + for (int i = 0; i < cpuLayout.cpus(); i++) { final boolean base = AffinityLock.BASE_AFFINITY.get(i); final boolean reservable = AffinityLock.RESERVED_AFFINITY.get(i); - - LOGGER.trace("cpu " + i + " base={} reservable= {}", i, base, reservable); - AffinityLock lock = logicalCoreLocks[i] = newLock(i, base, reservable); + LOGGER.trace("cpu {} base={} reservable= {}", i, base, reservable); + assert logicalCoreLocks != null; + @SuppressWarnings("resource") + AffinityLock lock = logicalCoreLocks[i] = newLock(i, cpuLayout.pair(i), base, reservable); int layoutId = lock.cpuId(); int physicalCore = toPhysicalCore(layoutId); @@ -77,24 +106,91 @@ public final synchronized void set(CpuLayout cpuLayout) { } locks[cpuLayout.threadId(layoutId)] = lock; } + shrink(physicalCoreLocks); + } + + /** + * If some CPUs are hyper-threaded, but not others, fix up the HT CPUs + */ + private void shrink(NavigableMap physicalCoreLocks) { + for (Map.Entry e : physicalCoreLocks.entrySet()) { + final AffinityLock[] locks = e.getValue(); + for (int i = 0; i < locks.length; i++) { + if (locks[i] == null) { + final AffinityLock[] locks2 = new AffinityLock[i]; + System.arraycopy(locks, 0, locks2, 0, i); + physicalCoreLocks.put(e.getKey(), locks2); + break; + } + } + } } public final synchronized AffinityLock acquireLock(boolean bind, int cpuId, AffinityStrategy... strategies) { - for (AffinityStrategy strategy : strategies) { - // consider all processors except cpu 0 which is usually used by the OS. - // if you have only one core, this library is not appropriate in any case. - for (int i = logicalCoreLocks.length - 1; i > 0; i--) { - AffinityLock al = logicalCoreLocks[i]; - if (al.canReserve() && (cpuId < 0 || strategy.matches(cpuId, al.cpuId()))) { - al.assignCurrentThread(bind, false); - return al; + if (getAffinityImpl() instanceof NullAffinity) + return noLock(); + + final boolean specificCpuRequested = !isAnyCpu(cpuId); + try { + if (specificCpuRequested && cpuId != 0) { + if (cpuId > logicalCoreLocks.length) { + LOGGER.warn("Unable to acquire lock on CPU {} for thread {}, as not enough CPUs", + cpuId, Thread.currentThread()); + return noLock(); + } + + final AffinityLock required = logicalCoreLocks[cpuId]; + if (required.canReserve(true) + && anyStrategyMatches(cpuId, cpuId, strategies) + && updateLockForCurrentThread(bind, required, false)) { + return required; } + LOGGER.warn("Unable to acquire lock on CPU {} for thread {}, trying to find another CPU", + cpuId, Thread.currentThread()); } + + for (AffinityStrategy strategy : strategies) { + // consider all processors except cpu 0 which is usually used by the OS. + // if you have only one core, this library is not appropriate in any case. + for (int i = logicalCoreLocks.length - 1; i > 0; i--) { + AffinityLock al = logicalCoreLocks[i]; + if (al.canReserve(false) + && (isAnyCpu(cpuId) || strategy.matches(cpuId, al.cpuId())) + && updateLockForCurrentThread(bind, al, false)) { + return al; + } + } + } + } catch (ClosedByInterruptException e) { + Thread.currentThread().interrupt(); + return noLock(); } LOGGER.warn("No reservable CPU for {}", Thread.currentThread()); - return newLock(-1, false, false); + return noLock(); + } + + public final synchronized AffinityLock tryAcquireLock(boolean bind, int cpuId) { + if (getAffinityImpl() instanceof NullAffinity) + return null; + if (cpuId > logicalCoreLocks.length) + return null; + final AffinityLock required = logicalCoreLocks[cpuId]; + try { + if (required.canReserve(true) + && updateLockForCurrentThread(bind, required, false)) { + return required; + } + } catch (ClosedByInterruptException e) { + Thread.currentThread().interrupt(); + return noLock(); + } + + LOGGER.warn("Unable to acquire lock on CPU {} for thread {}, trying to find another CPU", + cpuId, Thread.currentThread()); + + return null; } public final synchronized AffinityLock acquireCore(boolean bind, int cpuId, AffinityStrategy... strategies) { @@ -102,12 +198,18 @@ public final synchronized AffinityLock acquireCore(boolean bind, int cpuId, Affi LOOP: for (AffinityLock[] als : physicalCoreLocks.descendingMap().values()) { for (AffinityLock al : als) - if (!al.canReserve() || !strategy.matches(cpuId, al.cpuId())) + if (!al.canReserve(false) || !strategy.matches(cpuId, al.cpuId())) continue LOOP; final AffinityLock al = als[0]; - al.assignCurrentThread(bind, true); - return al; + try { + if (updateLockForCurrentThread(bind, al, true)) { + return al; + } + } catch (ClosedByInterruptException e) { + Thread.currentThread().interrupt(); + return noLock(); + } } } @@ -145,30 +247,26 @@ public final synchronized void bindWholeCore(int logicalCoreID) { } } - public final synchronized void release() { + public final synchronized void release(boolean resetAffinity) { Thread t = Thread.currentThread(); for (AffinityLock al : logicalCoreLocks) { Thread at = al.assignedThread; if (at == t) { - LOGGER.info("Releasing cpu {} from {}", al.cpuId(), t); - al.assignedThread = null; - al.bound = false; - + releaseAffinityLock(t, al, "Releasing cpu {} from {}"); } else if (at != null && !at.isAlive()) { - LOGGER.warn("Releasing cpu {} from {} as it is not alive.", al.cpuId(), t); - al.assignedThread = null; - al.bound = false; + releaseAffinityLock(t, al, "Releasing cpu {} from {} as it is not alive."); } } - Affinity.setAffinity(AffinityLock.BASE_AFFINITY); + if (resetAffinity) + Affinity.resetToBaseAffinity(); } public final synchronized String dumpLocks() { return dumpLocks(logicalCoreLocks); } - protected AffinityLock newLock(int cpuId, boolean base, boolean reservable) { - return new AffinityLock(cpuId, base, reservable, this); + protected AffinityLock newLock(int cpuId, int cpuId2, boolean base, boolean reservable) { + return new AffinityLock(cpuId, cpuId2, base, reservable, this); } private void reset(CpuLayout cpuLayout) { @@ -180,4 +278,17 @@ private void reset(CpuLayout cpuLayout) { private int toPhysicalCore(int layoutId) { return cpuLayout.socketId(layoutId) * cpuLayout.coresPerSocket() + cpuLayout.coreId(layoutId); } -} \ No newline at end of file + + private void releaseAffinityLock(final Thread t, final AffinityLock al, final String format) { + LOGGER.info(format, al.cpuId(), t); + al.assignedThread = null; + al.bound = false; + al.boundHere = null; + + LockCheck.releaseLock(al.cpuId()); + } + + public AffinityLock noLock() { + return newLock(AffinityLock.ANY_CPU, 0, false, false); + } +} diff --git a/affinity/src/main/java/net/openhft/affinity/MicroJitterSampler.java b/affinity/src/main/java/net/openhft/affinity/MicroJitterSampler.java new file mode 100644 index 000000000..30e27d221 --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/MicroJitterSampler.java @@ -0,0 +1,201 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import java.io.PrintStream; + +/** + * User: peter.lawrey Date: 30/06/13 Time: 13:13 + */ +public class MicroJitterSampler { + + private static final long[] DELAY = { + 2 * 1000, 3 * 1000, 4 * 1000, 6 * 1000, 8 * 1000, 10 * 1000, 14 * 1000, + 20 * 1000, 30 * 1000, 40 * 1000, 60 * 1000, 80 * 1000, 100 * 1000, 140 * 1000, + 200 * 1000, 300 * 1000, 400 * 1000, 600 * 1000, 800 * 1000, 1000 * 1000, + 2 * 1000 * 1000, 5 * 1000 * 1000, 10 * 1000 * 1000, + 20 * 1000 * 1000, 50 * 1000 * 1000, 100 * 1000 * 1000 + }; + private static final double UTIL = Double.parseDouble(System.getProperty("util", "50")); + private static final boolean BUSYWAIT = Boolean.parseBoolean(System.getProperty("busywait", "false")); + private static final String CPU = System.getProperty("cpu", "none"); + + private final int[] count = new int[DELAY.length]; + private long totalTime = 0; + + private static void pause() throws InterruptedException { + if (BUSYWAIT) { + long now = System.nanoTime(); + //noinspection StatementWithEmptyBody + while (System.nanoTime() - now < 1_000_000) ; + } else { + Thread.sleep(1); + } + } + + public static void main(String... ignored) throws InterruptedException { + MicroJitterSampler sampler = new MicroJitterSampler(); + + Thread t = new Thread(sampler::run); + t.start(); + t.join(); + } + + private void once() throws InterruptedException { + if (UTIL >= 100) { + sample(30L * 1000 * 1000 * 1000); + } else { + long sampleLength = (long) ((1 / (1 - UTIL / 100) - 1) * 1000 * 1000); + for (int i = 0; i < 30 * 1000; i += 2) { + sample(sampleLength); + //noinspection BusyWait + pause(); + } + } + } + + public void run() { + try (final AffinityLock lock = AffinityLock.acquireLock(CPU)) { + assert lock != null; + boolean first = true; + System.out.println("Warming up..."); + while (!Thread.currentThread().isInterrupted()) { + once(); + + if (first) { + reset(); + first = false; + System.out.println("Warmup complete. Running jitter tests..."); + continue; + } + + print(System.out); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + private static String asString(long timeNS) { + return timeNS < 1000 ? timeNS + "ns" : + timeNS < 1000000 ? timeNS / 1000 + "us" : + timeNS < 1000000000 ? timeNS / 1000000 + "ms" : + timeNS / 1000000000 + "sec"; + } + + void reset() { + for (int i = 0; i < DELAY.length; ++i) + count[i] = 0; + totalTime = 0; + } + + void sample(long intervalNS) { + long prev = System.nanoTime(); + long end = prev + intervalNS; + long now; + do { + now = System.nanoTime(); + long time = now - prev; + if (time >= DELAY[0]) { + int i; + for (i = 1; i < DELAY.length; i++) + if (time < DELAY[i]) + break; + count[i - 1]++; + } + prev = now; + } while (now < end); + totalTime += intervalNS; + } + + void print(PrintStream ps) { + ps.println("After " + totalTime / 1000000000 + " seconds, the average per hour was"); + for (int i = 0; i < DELAY.length; i++) { + if (count[i] < 1) continue; + long countPerHour = (long) Math.ceil(count[i] * 3600e9 / totalTime); + ps.println(asString(DELAY[i]) + '\t' + countPerHour); + } + ps.println(); + } +} +/* e.g. +Ubuntu 20.04, Ryzen 5950X with an isolated CPU. (init 3) sudo cpupower -c {cpu} -g performance, run from command line +After 3600 seconds, the average per hour was +2us 2571 +3us 2304 +4us 376 +6us 1 +10us 1 +20us 1 +30us 1 +40us 2 +60us 1 + +Ubuntu 20.04, Ryzen 5950X with an isolated CPU. (init 5) sudo cpupower -c {cpu} -g performance, run from command line +After 3600 seconds, the average per hour was +2us 2157 +3us 3444 +4us 3654 +6us 135 +8us 4 +14us 1 +20us 1 +40us 2 +60us 1 + +Ubuntu 20.04, Ryzen 5950X with an isolated CPU. (init 5) sudo cpupower -c {cpu} -g performance, run from IntelliJ CE +After 7200 seconds, the average per hour was +2us 2189 +3us 3341 +4us 2335 +6us 191 +8us 4 +14us 1 +20us 1 + +Windows 10 i7-4770 laptop +After 1845 seconds, the average per hour was +2us 2435969 +3us 548812 +4us 508041 +6us 60320 +8us 25374 +10us 1832324 +14us 2089216 +20us 391901 +30us 16063 +40us 6440 +60us 2617 +80us 1487 +100us 1241 +140us 826 +200us 2108 +300us 601 +400us 159 +600us 129 +800us 215 +1ms 155 +2ms 229 +5ms 24 +10ms 38 +20ms 32 + +On an Centos 7 machine with an isolated CPU. +After 2145 seconds, the average per hour was +2us 781271 +3us 1212123 +4us 13504 +6us 489 +8us 2 +10us 3032577 +14us 17475 +20us 628 +30us 645 +40us 1301 +60us 1217 +80us 1306 +100us 1526 +140us 22 + + */ diff --git a/affinity/src/main/java/net/openhft/affinity/NonForkingAffinityLock.java b/affinity/src/main/java/net/openhft/affinity/NonForkingAffinityLock.java deleted file mode 100644 index 84c950816..000000000 --- a/affinity/src/main/java/net/openhft/affinity/NonForkingAffinityLock.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package net.openhft.affinity; - -import net.openhft.affinity.impl.NoCpuLayout; -import org.jetbrains.annotations.NotNull; - -import java.lang.reflect.Field; - -public class NonForkingAffinityLock extends AffinityLock implements ThreadLifecycleListener { - - private static final Field GROUP_FIELD = makeThreadFieldModifiable("group"); - - private static final Field TARGET_FIELD = makeThreadFieldModifiable("target"); - - private static final LockInventory LOCK_INVENTORY = new LockInventory(new NoCpuLayout(PROCESSORS)) { - @Override - protected AffinityLock newLock(int cpuId, boolean base, boolean reservable) { - return new NonForkingAffinityLock(cpuId, base, reservable, this); - } - }; - - /** - * Assign any free cpu to this thread. - * - * @return A handle for the current AffinityLock. - */ - public static AffinityLock acquireLock() { - return acquireLock(true); - } - - /** - * Assign any free core to this thread. - *

- * In reality, only one cpu is assigned, the rest of the threads for that core are reservable so they are not used. - * - * @return A handle for the current AffinityLock. - */ - public static AffinityLock acquireCore() { - return acquireCore(true); - } - - /** - * Assign a cpu which can be bound to the current thread or another thread. - *

- * This can be used for defining your thread layout centrally and passing the handle via dependency injection. - * - * @param bind if true, bind the current thread, if false, reserve a cpu which can be bound later. - * @return A handle for an affinity lock. - */ - public static AffinityLock acquireLock(boolean bind) { - return acquireLock(bind, -1, AffinityStrategies.ANY); - } - - /** - * Assign a core(and all its cpus) which can be bound to the current thread or another thread. - *

- * This can be used for defining your thread layout centrally and passing the handle via dependency injection. - * - * @param bind if true, bind the current thread, if false, reserve a cpu which can be bound later. - * @return A handle for an affinity lock. - */ - public static AffinityLock acquireCore(boolean bind) { - return acquireCore(bind, -1, AffinityStrategies.ANY); - } - - private static AffinityLock acquireLock(boolean bind, int cpuId, @NotNull AffinityStrategy... strategies) { - return LOCK_INVENTORY.acquireLock(bind, cpuId, strategies); - } - - private static AffinityLock acquireCore(boolean bind, int cpuId, @NotNull AffinityStrategy... strategies) { - return LOCK_INVENTORY.acquireCore(bind, cpuId, strategies); - } - - /** - * Set the CPU layout for this machine. CPUs which are not mentioned will be ignored. - *

- * Changing the layout will have no impact on thread which have already been assigned. - * It only affects subsequent assignments. - * - * @param cpuLayout for this application to use for this machine. - */ - public static void cpuLayout(@NotNull CpuLayout cpuLayout) { - LOCK_INVENTORY.set(cpuLayout); - } - - /** - * @return The current CpuLayout for the application. - */ - @NotNull - public static CpuLayout cpuLayout() { - return LOCK_INVENTORY.getCpuLayout(); - } - - /** - * @return All the current locks as a String. - */ - @NotNull - public static String dumpLocks() { - return LOCK_INVENTORY.dumpLocks(); - } - - NonForkingAffinityLock(int cpuId, boolean base, boolean reservable, LockInventory lockInventory) { - super(cpuId, base, reservable, lockInventory); - } - - @Override - public void bind(boolean wholeCore) { - super.bind(wholeCore); - Thread thread = Thread.currentThread(); - changeGroupOfThread(thread, new ThreadTrackingGroup(thread.getThreadGroup(), this)); - } - - @Override - public void release() { - Thread thread = Thread.currentThread(); - changeGroupOfThread(thread, thread.getThreadGroup().getParent()); - super.release(); - } - - @Override - public void started(Thread t) { - wrapRunnableOfThread(t, this); - } - - @Override - public void startFailed(Thread t) { - } - - @Override - public void terminated(Thread t) { - } - - private static Field makeThreadFieldModifiable(String fieldName) { - try { - Field field = Thread.class.getDeclaredField(fieldName); - field.setAccessible(true); - return field; - } catch (NoSuchFieldException e) { - throw new RuntimeException(Thread.class.getName() + " class doesn't have a " + fieldName + " field! Quite unexpected!"); - } - } - - private static void changeGroupOfThread(Thread thread, ThreadGroup group) { - try { - GROUP_FIELD.set(thread, group); - } catch (IllegalAccessException e) { - throw new RuntimeException("Failed changing " + Thread.class.getName() + "'s the '" + GROUP_FIELD.getName() + "' field! Reason: " + e.getMessage()); - } - } - - private static void wrapRunnableOfThread(Thread thread, final AffinityLock lock) { - try { - final Runnable originalRunnable = (Runnable) TARGET_FIELD.get(thread); - TARGET_FIELD.set( - thread, - new Runnable() { - @Override - public void run() { - lock.release(); - originalRunnable.run(); - } - } - ); - } catch (IllegalAccessException e) { - throw new RuntimeException("Failed wrapping " + Thread.class.getName() + "'s '" + TARGET_FIELD.getName() + "' field! Reason: " + e.getMessage()); - } - } -} 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 1d095d596..a52404bba 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxHelper.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import com.sun.jna.*; @@ -22,16 +9,163 @@ import java.util.Arrays; import java.util.BitSet; +import java.util.Collections; import java.util.List; public class LinuxHelper { private static final String LIBRARY_NAME = "c"; - private static final VersionHelper UNKNOWN = new VersionHelper(0,0,0); - private static final VersionHelper VERSION_2_6 = new VersionHelper(2,6,0); + private static final VersionHelper UNKNOWN = new VersionHelper(0, 0, 0); + private static final VersionHelper VERSION_2_6 = new VersionHelper(2, 6, 0); private static final VersionHelper version; - /** Structure describing the system and machine. */ + static { + final utsname uname = new utsname(); + VersionHelper ver = UNKNOWN; + try { + if (CLibrary.INSTANCE.uname(uname) == 0) { + ver = new VersionHelper(uname.getRealeaseVersion()); + } + } catch (Throwable e) { + //Jvm.warn().on(getClass(), "Failed to determine Linux version: " + e); + } + + version = ver; + } + + public static + @NotNull + cpu_set_t sched_getaffinity() { + final CLibrary lib = CLibrary.INSTANCE; + 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; + + try { + if (lib.sched_getaffinity(0, size, cpuset) != 0) { + throw new IllegalStateException("sched_getaffinity(0, " + size + + ", cpuset) failed; errno=" + Native.getLastError()); + } + } catch (LastErrorException e) { + throw new IllegalStateException("sched_getaffinity(0, (" + + size + ") , cpuset) failed; errno=" + e.getErrorCode(), e); + } + return cpuset; + } + + public static void sched_setaffinity(final BitSet affinity) { + sched_setaffinity(0, affinity); + } + + public static void sched_setaffinity(final int pid, final BitSet affinity) { + final CLibrary lib = CLibrary.INSTANCE; + 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(); + for (int i = 0; i < bits.length; i++) { + if (Platform.is64Bit()) { + cpuset.__bits[i].setValue(bits[i]); + } else { + cpuset.__bits[i * 2].setValue(bits[i] & 0xFFFFFFFFL); + cpuset.__bits[i * 2 + 1].setValue((bits[i] >>> 32) & 0xFFFFFFFFL); + } + } + try { + if (lib.sched_setaffinity(pid, size, cpuset) != 0) { + throw new IllegalStateException("sched_setaffinity(" + pid + ", " + size + + ", 0x" + Utilities.toHexString(affinity) + ") failed; errno=" + Native.getLastError()); + } + } catch (LastErrorException e) { + throw new IllegalStateException("sched_setaffinity(" + pid + ", " + size + + ", 0x" + Utilities.toHexString(affinity) + ") failed; errno=" + e.getErrorCode(), e); + } + } + + public static int sched_getcpu() { + final CLibrary lib = CLibrary.INSTANCE; + try { + final int ret = lib.sched_getcpu(); + if (ret < 0) { + throw new IllegalStateException("sched_getcpu() failed; errno=" + Native.getLastError()); + } + return ret; + } catch (LastErrorException e) { + throw new IllegalStateException("sched_getcpu() failed; errno=" + e.getErrorCode(), e); + } catch (UnsatisfiedLinkError ule) { + try { + final IntByReference cpu = new IntByReference(); + final IntByReference node = new IntByReference(); + final int ret = lib.syscall(318, cpu, node, null); + if (ret != 0) { + throw new IllegalStateException("getcpu() failed; errno=" + Native.getLastError()); + } + return cpu.getValue(); + } catch (LastErrorException lee) { + if (lee.getErrorCode() == 38 && Platform.is64Bit()) { // unknown call + final Pointer getcpuAddr = new Pointer((-10L << 20) + 1024L * 2L); + final Function getcpu = Function.getFunction(getcpuAddr, Function.C_CONVENTION); + final IntByReference cpu = new IntByReference(); + if (getcpu.invokeInt(new Object[]{cpu, null, null}) < 0) { + throw new IllegalStateException("getcpu() failed; errno=" + Native.getLastError()); + + } else { + return cpu.getValue(); + } + } else { + throw new IllegalStateException("getcpu() failed; errno=" + lee.getErrorCode(), lee); + } + } + } + } + + public static int getpid() { + final CLibrary lib = CLibrary.INSTANCE; + try { + final int ret = lib.getpid(); + if (ret < 0) { + throw new IllegalStateException("getpid() failed; errno=" + Native.getLastError()); + } + return ret; + } catch (LastErrorException e) { + throw new IllegalStateException("getpid() failed; errno=" + e.getErrorCode(), e); + } + } + + public static int syscall(int number, Object... args) { + final CLibrary lib = CLibrary.INSTANCE; + try { + final int ret = lib.syscall(number, args); + if (ret < 0) { + throw new IllegalStateException("sched_getcpu() failed; errno=" + Native.getLastError()); + } + return ret; + } catch (LastErrorException e) { + throw new IllegalStateException("sched_getcpu() failed; errno=" + e.getErrorCode(), e); + } + } + + 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; + + int sched_getaffinity(final int pid, + final int cpusetsize, + final cpu_set_t cpuset) throws LastErrorException; + + int getpid() throws LastErrorException; + + int sched_getcpu() throws LastErrorException; + + int uname(final utsname name) throws LastErrorException; + + int syscall(int number, Object... args) throws LastErrorException; + } + + /** + * Structure describing the system and machine. + */ public static class utsname extends Structure { public static final int _UTSNAME_LENGTH = 65; @@ -44,42 +178,54 @@ public static class utsname extends Structure { "domainname" ); - /** Name of the implementation of the operating system. */ + /** + * Name of the implementation of the operating system. + */ public byte[] sysname = new byte[_UTSNAME_LENGTH]; - /** Name of this node on the network. */ + /** + * Name of this node on the network. + */ public byte[] nodename = new byte[_UTSNAME_LENGTH]; - /** Current release level of this implementation. */ + /** + * Current release level of this implementation. + */ public byte[] release = new byte[_UTSNAME_LENGTH]; - /** Current version level of this release. */ + /** + * Current version level of this release. + */ public byte[] version = new byte[_UTSNAME_LENGTH]; - /** Name of the hardware type the system is running on. */ + /** + * Name of the hardware type the system is running on. + */ public byte[] machine = new byte[_UTSNAME_LENGTH]; - /** NIS or YP domain name */ + /** + * NIS or YP domain name + */ public byte[] domainname = new byte[_UTSNAME_LENGTH]; - @Override - protected List getFieldOrder() { - return FIELD_ORDER; - } - static int length(final byte[] data) { int len = 0; final int datalen = data.length; - while(len < datalen && data[len] != 0) + while (len < datalen && data[len] != 0) len++; return len; } + @Override + protected List getFieldOrder() { + return FIELD_ORDER; + } + public String getSysname() { return new String(sysname, 0, length(sysname)); } - @SuppressWarnings({"UnusedDeclaration"}) + @SuppressWarnings("unused") public String getNodename() { return new String(nodename, 0, length(nodename)); } @@ -92,9 +238,9 @@ public String getRealeaseVersion() { final String release = getRelease(); final int releaseLen = release.length(); int len = 0; - for(;len < releaseLen; len++) { + for (; len < releaseLen; len++) { final char c = release.charAt(len); - if(Character.isDigit(c) || c == '.') { + if (Character.isDigit(c) || c == '.') { continue; } break; @@ -110,7 +256,7 @@ public String getMachine() { return new String(machine, 0, length(machine)); } - @SuppressWarnings({"UnusedDeclaration"}) + @SuppressWarnings("UnusedDeclaration") public String getDomainname() { return new String(domainname, 0, length(domainname)); } @@ -122,41 +268,23 @@ public String toString() { } } - static { - final utsname uname = new utsname(); - VersionHelper ver = UNKNOWN; - try { - if(CLibrary.INSTANCE.uname(uname) == 0) { - ver = new VersionHelper(uname.getRealeaseVersion()); - } - } catch(Throwable e) { - //logger.warn("Failed to determine Linux version: " + e); - } - - version = ver; - } - public static class cpu_set_t extends Structure { - static List FIELD_ORDER = Arrays.asList("__bits"); - static final int __CPU_SETSIZE = 1024; + 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 List FIELD_ORDER = Collections.singletonList("__bits"); public NativeLong[] __bits = new NativeLong[__CPU_SETSIZE / __NCPUBITS]; + public cpu_set_t() { - for(int i = 0; i < __bits.length; i++) { + for (int i = 0; i < __bits.length; i++) { __bits[i] = new NativeLong(0); } } - @Override - protected List getFieldOrder() { - return FIELD_ORDER; - } - - @SuppressWarnings({"UnusedDeclaration"}) + @SuppressWarnings("UnusedDeclaration") public static void __CPU_ZERO(cpu_set_t cpuset) { - for(NativeLong bits : cpuset.__bits) { - bits.setValue(0l); + for (NativeLong bits : cpuset.__bits) { + bits.setValue(0L); } } @@ -165,148 +293,29 @@ public static int __CPUELT(int cpu) { } public static long __CPUMASK(int cpu) { - return 1l << (cpu % __NCPUBITS); + return 1L << (cpu % __NCPUBITS); } - @SuppressWarnings({"UnusedDeclaration"}) - public static void __CPU_SET(int cpu, cpu_set_t cpuset ) { + @SuppressWarnings("UnusedDeclaration") + public static void __CPU_SET(int cpu, cpu_set_t cpuset) { cpuset.__bits[__CPUELT(cpu)].setValue( cpuset.__bits[__CPUELT(cpu)].longValue() | __CPUMASK(cpu)); } - @SuppressWarnings({"UnusedDeclaration"}) - public static void __CPU_CLR(int cpu, cpu_set_t cpuset ) { + @SuppressWarnings("UnusedDeclaration") + public static void __CPU_CLR(int cpu, cpu_set_t cpuset) { cpuset.__bits[__CPUELT(cpu)].setValue( cpuset.__bits[__CPUELT(cpu)].longValue() & ~__CPUMASK(cpu)); } - @SuppressWarnings({"UnusedDeclaration"}) - public static boolean __CPU_ISSET(int cpu, cpu_set_t cpuset ) { + @SuppressWarnings("UnusedDeclaration") + public static boolean __CPU_ISSET(int cpu, cpu_set_t cpuset) { return (cpuset.__bits[__CPUELT(cpu)].longValue() & __CPUMASK(cpu)) != 0; } - } - - interface CLibrary extends Library { - CLibrary INSTANCE = (CLibrary) Native.loadLibrary(LIBRARY_NAME, CLibrary.class); - - int sched_setaffinity(final int pid, - final int cpusetsize, - final cpu_set_t cpuset) throws LastErrorException; - - int sched_getaffinity(final int pid, - final int cpusetsize, - final cpu_set_t cpuset) throws LastErrorException; - - int getpid() throws LastErrorException; - - int sched_getcpu() throws LastErrorException; - - int uname(final utsname name) throws LastErrorException; - - int syscall(int number, Object... args) throws LastErrorException; - } - - public static @NotNull cpu_set_t sched_getaffinity() { - final CLibrary lib = CLibrary.INSTANCE; - 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; - - try { - if(lib.sched_getaffinity(0, size, cpuset) != 0) { - throw new IllegalStateException("sched_getaffinity(0, " + size + - ", cpuset) failed; errno=" + Native.getLastError()); - } - } catch (LastErrorException e) { - throw new IllegalStateException("sched_getaffinity(0, (" + - size + ") , cpuset) failed; errno=" + e.getErrorCode(), e); - } - return cpuset; - } - - public static void sched_setaffinity(final BitSet affinity) { - final CLibrary lib = CLibrary.INSTANCE; - 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(); - for (int i = 0; i < bits.length; i++) { - if (Platform.is64Bit()) { - cpuset.__bits[i].setValue(bits[i]); - } else { - cpuset.__bits[i*2].setValue(bits[i] & 0xFFFFFFFFL); - cpuset.__bits[i*2+1].setValue((bits[i] >>> 32) & 0xFFFFFFFFL); - } - } - try { - if(lib.sched_setaffinity(0, size, cpuset) != 0) { - throw new IllegalStateException("sched_setaffinity(0, " + size + - ", 0x" + Utilities.toHexString(affinity) + ") failed; errno=" + Native.getLastError()); - } - } catch (LastErrorException e) { - throw new IllegalStateException("sched_setaffinity(0, " + size + - ", 0x" + Utilities.toHexString(affinity) + ") failed; errno=" + e.getErrorCode(), e); - } - } - - public static int sched_getcpu() { - final CLibrary lib = CLibrary.INSTANCE; - try { - final int ret = lib.sched_getcpu(); - if(ret < 0) { - throw new IllegalStateException("sched_getcpu() failed; errno=" + Native.getLastError()); - } - return ret; - } catch (LastErrorException e) { - throw new IllegalStateException("sched_getcpu() failed; errno=" + e.getErrorCode(), e); - } catch (UnsatisfiedLinkError ule) { - try { - final IntByReference cpu = new IntByReference(); - final IntByReference node = new IntByReference(); - final int ret = lib.syscall(318, cpu, node, null); - if (ret != 0) { - throw new IllegalStateException("getcpu() failed; errno=" + Native.getLastError()); - } - return cpu.getValue(); - } catch (LastErrorException lee) { - if(lee.getErrorCode() == 38 && Platform.is64Bit()) { // unknown call - final Pointer getcpuAddr = new Pointer((-10L << 20) + 1024L * 2L); - final Function getcpu = Function.getFunction(getcpuAddr, Function.C_CONVENTION); - final IntByReference cpu = new IntByReference(); - if(getcpu.invokeInt(new Object[] { cpu, null, null }) < 0) { - throw new IllegalStateException("getcpu() failed; errno=" + Native.getLastError()); - - } else { - return cpu.getValue(); - } - } else { - throw new IllegalStateException("getcpu() failed; errno=" + lee.getErrorCode(), lee); - } - } - } - } - public static int getpid() { - final CLibrary lib = CLibrary.INSTANCE; - try { - final int ret = lib.getpid(); - if(ret < 0) { - throw new IllegalStateException("getpid() failed; errno=" + Native.getLastError()); - } - return ret; - } catch (LastErrorException e) { - throw new IllegalStateException("getpid() failed; errno=" + e.getErrorCode(), e); - } - } - - public static int syscall(int number, Object... args) { - final CLibrary lib = CLibrary.INSTANCE; - try { - final int ret = lib.syscall(number, args); - if (ret < 0) { - throw new IllegalStateException("sched_getcpu() failed; errno=" + Native.getLastError()); - } - return ret; - } catch (LastErrorException e) { - throw new IllegalStateException("sched_getcpu() failed; errno=" + e.getErrorCode(), e); + @Override + protected List getFieldOrder() { + return FIELD_ORDER; } } } 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 e1732740d..d16fc3beb 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/LinuxJNAAffinity.java @@ -1,99 +1,63 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; +import com.sun.jna.NativeLong; import com.sun.jna.Platform; import net.openhft.affinity.IAffinity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.ByteBuffer; -import java.util.ArrayList; import java.util.BitSet; -import java.util.Collections; -import java.util.LinkedList; public enum LinuxJNAAffinity implements IAffinity { INSTANCE; - private static final Logger LOGGER = LoggerFactory.getLogger(LinuxJNAAffinity.class); public static final boolean LOADED; + private static final Logger LOGGER = LoggerFactory.getLogger(LinuxJNAAffinity.class); + private static final int PROCESS_ID; + private static final int SYS_gettid = Platform.isPPC() ? 207 : Platform.is64Bit() ? 186 : 224; + private static final Object[] NO_ARGS = {}; - // TODO: FIXME!!! CHANGE IAffinity TO SUPPORT PLATFORMS WITH 64+ CORES FIXME!!! - @Override - public BitSet getAffinity() { - final LinuxHelper.cpu_set_t cpuset = LinuxHelper.sched_getaffinity(); - - boolean collect = false; - ArrayList bytes = new ArrayList(); + private static final String OS = System.getProperty("os.name").toLowerCase(); + private static final boolean IS_LINUX = OS.startsWith("linux"); - ByteBuffer buff = null; - if (Platform.is64Bit()) - { - buff = ByteBuffer.allocate(Long.SIZE / 8); - } - else - { - buff = ByteBuffer.allocate(Integer.SIZE / 8); + static { + int pid = -1; + try { + pid = LinuxHelper.getpid(); + } catch (NoClassDefFoundError | Exception ignored) { } + PROCESS_ID = pid; + } - for (int i = cpuset.__bits.length - 1; i >= 0; --i) - { - if (!collect && cpuset.__bits[i].longValue() != 0) - { - collect = true; - } + static { + boolean loaded = false; + try { + INSTANCE.getAffinity(); + loaded = true; + } catch (NoClassDefFoundError | UnsatisfiedLinkError e) { + if (IS_LINUX) + LOGGER.warn("Unable to load jna library", e); + } + LOADED = loaded; + } - if (collect) - { - if (Platform.is64Bit()) - { - buff.putLong(cpuset.__bits[i].longValue()); - } - else - { - buff.putInt((int) cpuset.__bits[i].longValue()); - } + private final ThreadLocal THREAD_ID = new ThreadLocal<>(); - final byte[] arr = buff.array(); - //for (int j = arr.length - 1; j >= 0; --j) - for (int j = 0; j < arr.length; j++) - { - bytes.add(arr[j]); - } - } - } + @Override + public BitSet getAffinity() { + final LinuxHelper.cpu_set_t cpuset = LinuxHelper.sched_getaffinity(); - if (!bytes.isEmpty()) - { - byte[] data = new byte[bytes.size()]; - for (int i = 0; i < bytes.size(); i++) - { - // don't forget to reverse the order of long values - data[data.length - i - 1] = bytes.get(i); - } - return BitSet.valueOf(data); - } - else - { - return new BitSet(); + BitSet ret = new BitSet(LinuxHelper.cpu_set_t.__CPU_SETSIZE); + int i = 0; + for (NativeLong nl : cpuset.__bits) { + for (int j = 0; j < Long.SIZE; j++) + ret.set(i++, ((nl.longValue() >>> j) & 1) != 0); } + return ret; } - // TODO: FIXME!!! CHANGE IAffinity TO SUPPORT PLATFORMS WITH 64+ CORES FIXME!!! @Override public void setAffinity(final BitSet affinity) { LinuxHelper.sched_setaffinity(affinity); @@ -104,25 +68,11 @@ public int getCpu() { return LinuxHelper.sched_getcpu(); } - private static final int PROCESS_ID; - static { - int pid = -1; - try { - pid = LinuxHelper.getpid(); - } catch (Exception ignored) { - } - PROCESS_ID = pid; - } - @Override public int getProcessId() { return PROCESS_ID; } - private static final int SYS_gettid = Platform.is64Bit() ? 186 : 224; - private static final Object[] NO_ARGS = {}; - private final ThreadLocal THREAD_ID = new ThreadLocal<>(); - @Override public int getThreadId() { Integer tid = THREAD_ID.get(); @@ -130,15 +80,4 @@ public int getThreadId() { THREAD_ID.set(tid = LinuxHelper.syscall(SYS_gettid, NO_ARGS)); return tid; } - - static { - boolean loaded = false; - try { - INSTANCE.getAffinity(); - loaded = true; - } catch (UnsatisfiedLinkError e) { - LOGGER.warn("Unable to load jna library {}", e); - } - LOADED = loaded; - } } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/NoCpuLayout.java b/affinity/src/main/java/net/openhft/affinity/impl/NoCpuLayout.java index eadc7247a..e57b1decc 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/NoCpuLayout.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/NoCpuLayout.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import net.openhft.affinity.CpuLayout; @@ -63,4 +50,9 @@ public int coreId(int cpuId) { public int threadId(int cpuId) { return 0; } + + @Override + public int pair(int cpuId) { + return 0; + } } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/NullAffinity.java b/affinity/src/main/java/net/openhft/affinity/impl/NullAffinity.java index 4118c367e..4c5e8e304 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/NullAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/NullAffinity.java @@ -1,26 +1,12 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import net.openhft.affinity.IAffinity; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.management.ManagementFactory; import java.util.BitSet; /** @@ -37,7 +23,7 @@ public BitSet getAffinity() { @Override public void setAffinity(final BitSet affinity) { - LOGGER.trace("unable to set mask to {} as the JNIa nd JNA libraries and not loaded", Utilities.toHexString(affinity)); + LOGGER.trace("unable to set mask to {} as the JNI and JNA libraries not loaded", Utilities.toHexString(affinity)); } @Override @@ -47,8 +33,7 @@ public int getCpu() { @Override public int getProcessId() { - final String name = ManagementFactory.getRuntimeMXBean().getName(); - return Integer.parseInt(name.split("@")[0]); + return Utilities.currentProcessId(); } @Override 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 41e1278d4..fd020fd21 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/OSXJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/OSXJNAAffinity.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import com.sun.jna.LastErrorException; @@ -23,12 +10,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.management.ManagementFactory; import java.util.BitSet; /** * This is essentially the same as the NullAffinity implementation but with concrete * support for getThreadId(). + * * @author daniel.shaya */ public enum OSXJNAAffinity implements IAffinity { @@ -37,14 +24,13 @@ public enum OSXJNAAffinity implements IAffinity { private final ThreadLocal THREAD_ID = new ThreadLocal<>(); @Override - public BitSet getAffinity() - { + public BitSet getAffinity() { return new BitSet(); } @Override public void setAffinity(final BitSet affinity) { - LOGGER.trace("unable to set mask to {} as the JNIa nd JNA libraries and not loaded", Utilities.toHexString(affinity)); + LOGGER.trace("unable to set mask to {} as the JNI and JNA libraries not loaded", Utilities.toHexString(affinity)); } @Override @@ -54,8 +40,7 @@ public int getCpu() { @Override public int getProcessId() { - final String name = ManagementFactory.getRuntimeMXBean().getName(); - return Integer.parseInt(name.split("@")[0]); + return Utilities.currentProcessId(); } @Override @@ -71,8 +56,7 @@ public int getThreadId() { } interface CLibrary extends Library { - CLibrary INSTANCE = (CLibrary) - Native.loadLibrary("libpthread.dylib", CLibrary.class); + CLibrary INSTANCE = Native.load("libpthread.dylib", CLibrary.class); int pthread_self() throws LastErrorException; } 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 36c62a294..39aed93ab 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/PosixJNAAffinity.java @@ -1,24 +1,10 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import com.sun.jna.*; import com.sun.jna.ptr.IntByReference; -import com.sun.jna.ptr.LongByReference; import com.sun.jna.ptr.PointerByReference; import net.openhft.affinity.IAffinity; import org.slf4j.Logger; @@ -43,10 +29,35 @@ public enum PosixJNAAffinity implements IAffinity { public static final boolean LOADED; private static final Logger LOGGER = LoggerFactory.getLogger(PosixJNAAffinity.class); private static final String LIBRARY_NAME = Platform.isWindows() ? "msvcrt" : "c"; + private static final int PROCESS_ID; + private static final int SYS_gettid = Utilities.is64Bit() ? 186 : 224; + private static final Object[] NO_ARGS = {}; + + static { + int processId; + try { + processId = CLibrary.INSTANCE.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); + } + LOADED = loaded; + } + + private final ThreadLocal THREAD_ID = new ThreadLocal<>(); @Override - public BitSet getAffinity() - { + public BitSet getAffinity() { final CLibrary lib = CLibrary.INSTANCE; final int procs = Runtime.getRuntime().availableProcessors(); @@ -56,36 +67,28 @@ public BitSet getAffinity() final PointerByReference cpuset = new PointerByReference(cpusetArray); try { final int ret = lib.sched_getaffinity(0, cpuSetSizeInBytes, cpuset); - if (ret < 0) - { + if (ret < 0) { throw new IllegalStateException("sched_getaffinity((" + cpuSetSizeInBytes + ") , &(" + cpusetArray + ") ) return " + ret); } ByteBuffer buff = cpusetArray.getByteBuffer(0, cpuSetSizeInBytes); return BitSet.valueOf(buff.array()); - } - catch (LastErrorException e) - { - if (e.getErrorCode() != 22) - { + } catch (LastErrorException e) { + if (e.getErrorCode() != 22) { throw new IllegalStateException("sched_getaffinity((" + cpuSetSizeInBytes + ") , &(" + cpusetArray + ") ) errorNo=" + e.getErrorCode(), e); } } // fall back to the old method final IntByReference cpuset32 = new IntByReference(0); - try - { + try { final int ret = lib.sched_getaffinity(0, Integer.SIZE / 8, cpuset32); - if (ret < 0) - { + if (ret < 0) { throw new IllegalStateException("sched_getaffinity((" + Integer.SIZE / 8 + ") , &(" + cpuset32 + ") ) return " + ret); } long[] longs = new long[1]; longs[0] = cpuset32.getValue() & 0xFFFFFFFFL; return BitSet.valueOf(longs); - } - catch (LastErrorException e) - { + } catch (LastErrorException e) { throw new IllegalStateException("sched_getaffinity((" + Integer.SIZE / 8 + ") , &(" + cpuset32 + ") ) errorNo=" + e.getErrorCode(), e); } } @@ -93,8 +96,7 @@ public BitSet getAffinity() @Override public void setAffinity(final BitSet affinity) { int procs = Runtime.getRuntime().availableProcessors(); - if (affinity.isEmpty()) - { + if (affinity.isEmpty()) { throw new IllegalArgumentException("Cannot set zero affinity"); } @@ -102,26 +104,20 @@ public void setAffinity(final BitSet affinity) { byte[] buff = affinity.toByteArray(); final int cpuSetSizeInBytes = buff.length; final Memory cpusetArray = new Memory(cpuSetSizeInBytes); - try - { + try { cpusetArray.write(0, buff, 0, buff.length); final int ret = lib.sched_setaffinity(0, cpuSetSizeInBytes, new PointerByReference(cpusetArray)); - if (ret < 0) - { + if (ret < 0) { throw new IllegalStateException("sched_setaffinity((" + cpuSetSizeInBytes + ") , &(" + affinity + ") ) return " + ret); } - } - catch (LastErrorException e) - { - if (e.getErrorCode() != 22 || !Arrays.equals(buff, cpusetArray.getByteArray(0, cpuSetSizeInBytes))) - { + } catch (LastErrorException e) { + if (e.getErrorCode() != 22 || !Arrays.equals(buff, cpusetArray.getByteArray(0, cpuSetSizeInBytes))) { throw new IllegalStateException("sched_setaffinity((" + cpuSetSizeInBytes + ") , &(" + affinity + ") ) errorNo=" + e.getErrorCode(), e); } } final int value = (int) affinity.toLongArray()[0]; - if (value == 0) - { + if (value == 0) { throw new IllegalArgumentException("Cannot set zero affinity"); } final IntByReference cpuset32 = new IntByReference(0); @@ -146,7 +142,7 @@ public int getCpu() { } catch (LastErrorException e) { throw new IllegalStateException("sched_getcpu( ) errorNo=" + e.getErrorCode(), e); } catch (UnsatisfiedLinkError ule) { - try { + try { final IntByReference cpu = new IntByReference(); final IntByReference node = new IntByReference(); final int ret = lib.syscall(318, cpu, node, null); @@ -161,28 +157,14 @@ public int getCpu() { } } - private static final int PROCESS_ID; - @Override public int getProcessId() { return PROCESS_ID; } - static { - int processId; - try { - processId = CLibrary.INSTANCE.getpid(); - } catch (Exception ignored) { - processId = -1; - } - PROCESS_ID = processId; - } - - private final ThreadLocal THREAD_ID = new ThreadLocal(); - @Override public int getThreadId() { - if (ISLINUX) { + if (Utilities.ISLINUX) { Integer tid = THREAD_ID.get(); if (tid == null) THREAD_ID.set(tid = CLibrary.INSTANCE.syscall(SYS_gettid, NO_ARGS)); @@ -191,38 +173,11 @@ public int getThreadId() { return -1; } - private static final boolean ISLINUX = "Linux".equals(System.getProperty("os.name")); - - private static final boolean IS64BIT = is64Bit0(); - - private static final int SYS_gettid = is64Bit() ? 186 : 224; - - private static final Object[] NO_ARGS = {}; - - public static boolean is64Bit() { - return IS64BIT; - } - - private static boolean is64Bit0() { - String systemProp; - systemProp = System.getProperty("com.ibm.vm.bitmode"); - if (systemProp != null) { - return "64".equals(systemProp); - } - systemProp = System.getProperty("sun.arch.data.model"); - if (systemProp != null) { - return "64".equals(systemProp); - } - systemProp = System.getProperty("java.vm.version"); - return systemProp != null && systemProp.contains("_64"); - } - /** * @author BegemoT */ interface CLibrary extends Library { - CLibrary INSTANCE = (CLibrary) - Native.loadLibrary(LIBRARY_NAME, CLibrary.class); + CLibrary INSTANCE = Native.load(LIBRARY_NAME, CLibrary.class); int sched_setaffinity(final int pid, final int cpusetsize, @@ -233,7 +188,7 @@ int sched_getaffinity(final int pid, final PointerType cpuset) throws LastErrorException; int sched_getcpu() throws LastErrorException; - + int getcpu(final IntByReference cpu, final IntByReference node, final PointerType tcache) throws LastErrorException; @@ -242,15 +197,4 @@ int getcpu(final IntByReference cpu, int syscall(int number, Object... args) throws LastErrorException; } - - static { - boolean loaded = false; - try { - INSTANCE.getAffinity(); - loaded = true; - } catch (UnsatisfiedLinkError e) { - LOGGER.warn("Unable to load jna library {}", e); - } - LOADED = loaded; - } } 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 020c991aa..fcb5b8db0 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/SolarisJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/SolarisJNAAffinity.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import com.sun.jna.LastErrorException; @@ -23,12 +10,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.lang.management.ManagementFactory; import java.util.BitSet; /** * This is essentially the same as the NullAffinity implementation but with concrete * support for getThreadId(). + * * @author daniel.shaya */ public enum SolarisJNAAffinity implements IAffinity { @@ -37,14 +24,13 @@ public enum SolarisJNAAffinity implements IAffinity { private final ThreadLocal THREAD_ID = new ThreadLocal<>(); @Override - public BitSet getAffinity() - { + public BitSet getAffinity() { return new BitSet(); } @Override public void setAffinity(final BitSet affinity) { - LOGGER.trace("unable to set mask to {} as the JNIa nd JNA libraries and not loaded", Utilities.toHexString(affinity)); + LOGGER.trace("unable to set mask to {} as the JNI and JNA libraries not loaded", Utilities.toHexString(affinity)); } @Override @@ -54,8 +40,7 @@ public int getCpu() { @Override public int getProcessId() { - final String name = ManagementFactory.getRuntimeMXBean().getName(); - return Integer.parseInt(name.split("@")[0]); + return Utilities.currentProcessId(); } @Override @@ -71,8 +56,7 @@ public int getThreadId() { } interface CLibrary extends Library { - CLibrary INSTANCE = (CLibrary) - Native.loadLibrary("c", CLibrary.class); + CLibrary INSTANCE = Native.load("c", CLibrary.class); int pthread_self() throws LastErrorException; } 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 a008006c8..babf6b360 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/Utilities.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/Utilities.java @@ -1,44 +1,94 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ package net.openhft.affinity.impl; import java.io.ByteArrayOutputStream; import java.io.PrintWriter; import java.util.BitSet; -/** +/* * Created by andre on 20/06/15. */ -public class Utilities -{ +public final class Utilities { + public static final boolean ISLINUX = "Linux".equals(System.getProperty("os.name")); + static final boolean IS64BIT = is64Bit0(); + + private Utilities() { + throw new InstantiationError("Must not instantiate this class"); + } + /** * Creates a hexademical representation of the bit set + * * @param set the bit set to convert * @return the hexademical string representation */ - public static String toHexString(final BitSet set) - { + public static String toHexString(final BitSet set) { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(out); final long[] longs = set.toLongArray(); - for (int i = 0; i < longs.length; i++) - { - writer.write(Long.toHexString(longs[i])); + for (long aLong : longs) { + writer.write(Long.toHexString(aLong)); } writer.flush(); return new String(out.toByteArray(), java.nio.charset.StandardCharsets.UTF_8); } - public static String toBinaryString(BitSet set) - { + public static String toBinaryString(BitSet set) { ByteArrayOutputStream out = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(out); final long[] longs = set.toLongArray(); - for (int i = 0; i < longs.length; i++) - { - writer.write(Long.toBinaryString(longs[i])); + for (long aLong : longs) { + writer.write(Long.toBinaryString(aLong)); } writer.flush(); return new String(out.toByteArray(), java.nio.charset.StandardCharsets.UTF_8); } + + public static boolean is64Bit() { + return IS64BIT; + } + + private static boolean is64Bit0() { + String systemProp; + systemProp = System.getProperty("com.ibm.vm.bitmode"); + if (systemProp != null) { + return "64".equals(systemProp); + } + systemProp = System.getProperty("sun.arch.data.model"); + if (systemProp != null) { + return "64".equals(systemProp); + } + systemProp = System.getProperty("java.vm.version"); + return systemProp != null && systemProp.contains("_64"); + } + + /** + * Returns the current process id. Uses {@code ProcessHandle} when running + * on Java 9 or later and falls back to parsing + * {@code RuntimeMXBean#getName()} on earlier versions. + * + * @return the process id or {@code -1} if it cannot be determined + */ + public static int currentProcessId() { + try { + // Java 9+ provides ProcessHandle which has a pid() method. + Class phClass = Class.forName("java.lang.ProcessHandle"); + Object current = phClass.getMethod("current").invoke(null); + long pid = (Long) phClass.getMethod("pid").invoke(current); + return (int) pid; + } catch (Throwable ignored) { + // ignore and fallback to the pre-Java 9 approach + } + + try { + String name = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); + return Integer.parseInt(name.split("@")[0]); + } catch (Throwable e) { + return -1; + } + } } diff --git a/affinity/src/main/java/net/openhft/affinity/impl/VanillaCpuLayout.java b/affinity/src/main/java/net/openhft/affinity/impl/VanillaCpuLayout.java index 28970076f..4fac94fe4 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/VanillaCpuLayout.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/VanillaCpuLayout.java @@ -1,26 +1,15 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import net.openhft.affinity.CpuLayout; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.LoggerFactory; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.*; import static java.lang.Integer.parseInt; @@ -29,7 +18,7 @@ * @author peter.lawrey */ public class VanillaCpuLayout implements CpuLayout { - public static final int MAX_CPUS_SUPPORTED = 64; + public static final int MAX_CPUS_SUPPORTED = 256; @NotNull private final List cpuDetails; @@ -39,9 +28,9 @@ public class VanillaCpuLayout implements CpuLayout { VanillaCpuLayout(@NotNull List cpuDetails) { this.cpuDetails = cpuDetails; - SortedSet sockets = new TreeSet(), - cores = new TreeSet(), - threads = new TreeSet(); + SortedSet sockets = new TreeSet<>(), + cores = new TreeSet<>(), + threads = new TreeSet<>(); for (CpuInfo cpuDetail : cpuDetails) { sockets.add(cpuDetail.socketId); cores.add((cpuDetail.socketId << 16) + cpuDetail.coreId); @@ -59,7 +48,7 @@ public class VanillaCpuLayout implements CpuLayout { for (CpuInfo detail : cpuDetails) { error.append(detail).append('\n'); } - throw new AssertionError(error); + LoggerFactory.getLogger(VanillaCpuLayout.class).warn(error.toString()); } } @@ -77,7 +66,7 @@ public static VanillaCpuLayout fromProperties(InputStream is) throws IOException @NotNull public static VanillaCpuLayout fromProperties(@NotNull Properties prop) { - List cpuDetails = new ArrayList(); + List cpuDetails = new ArrayList<>(); for (int i = 0; i < MAX_CPUS_SUPPORTED; i++) { String line = prop.getProperty("" + i); if (line == null) break; @@ -112,11 +101,11 @@ private static InputStream openFile(String filename) throws FileNotFoundExceptio @NotNull public static VanillaCpuLayout fromCpuInfo(InputStream is) throws IOException { - BufferedReader br = new BufferedReader(new InputStreamReader(is, "UTF-8")); + BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); String line; - List cpuDetails = new ArrayList(); + List cpuDetails = new ArrayList<>(); CpuInfo details = new CpuInfo(); - Map threadCount = new LinkedHashMap(); + Map threadCount = new LinkedHashMap<>(); while ((line = br.readLine()) != null) { if (line.trim().isEmpty()) { @@ -174,6 +163,19 @@ public int threadId(int cpuId) { return cpuDetails.get(cpuId).threadId; } + @Override + public int pair(int cpuId) { + for (int i = 0; i < cpuDetails.size(); i++) { + CpuInfo info = cpuDetails.get(i); + if (info.socketId == cpuDetails.get(cpuId).socketId && + info.coreId == cpuDetails.get(cpuId).coreId && + info.threadId != cpuDetails.get(cpuId).threadId) { + return i; + } + } + return 0; + } + @NotNull @Override public String toString() { diff --git a/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java b/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java index 271490343..08a01ab19 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; public class VersionHelper { @@ -29,7 +16,7 @@ public VersionHelper(int major_, int minor_, int release_) { } public VersionHelper(String ver) { - if(ver != null && (ver = ver.trim()).length() > 0) { + if (ver != null && !(ver = ver.trim()).isEmpty()) { final String[] parts = ver.split("\\."); major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; @@ -45,8 +32,8 @@ public String toString() { } public boolean equals(Object o) { - if(o != null && (o instanceof VersionHelper)) { - VersionHelper ver = (VersionHelper)o; + if (o instanceof VersionHelper) { + VersionHelper ver = (VersionHelper) o; return this.major == ver.major && this.minor == ver.minor && this.release == ver.release; @@ -60,18 +47,19 @@ public int hashCode() { return (major << 16) | (minor << 8) | release; } - @SuppressWarnings({"UnusedDeclaration"}) + @SuppressWarnings("unused") public boolean majorMinorEquals(final VersionHelper ver) { return ver != null - && this.major == ver.major - && this.minor == ver.minor; + && this.major == ver.major + && this.minor == ver.minor; } public boolean isSameOrNewer(final VersionHelper ver) { return ver != null - && this.major >= ver.major - && this.minor >= ver.minor - && this.release >= ver.release; + && (this.major > ver.major + || this.major == ver.major + && (this.minor > ver.minor + || this.minor == ver.minor + && this.release >= ver.release)); } } - 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 6cb5fed40..bdeef6710 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -1,29 +1,15 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; -import com.sun.jna.LastErrorException; -import com.sun.jna.Library; -import com.sun.jna.Native; -import com.sun.jna.PointerType; +import com.sun.jna.*; import com.sun.jna.platform.win32.Kernel32; import com.sun.jna.platform.win32.WinDef; +import com.sun.jna.platform.win32.WinNT; import com.sun.jna.ptr.LongByReference; import net.openhft.affinity.IAffinity; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +27,7 @@ public enum WindowsJNAAffinity implements IAffinity { INSTANCE; public static final boolean LOADED; private static final Logger LOGGER = LoggerFactory.getLogger(WindowsJNAAffinity.class); + private static final ThreadLocal currentAffinity = new ThreadLocal<>(); static { boolean loaded = false; @@ -57,29 +44,11 @@ public enum WindowsJNAAffinity implements IAffinity { @Override public BitSet getAffinity() { - final CLibrary lib = CLibrary.INSTANCE; - final LongByReference cpuset1 = new LongByReference(0); - final LongByReference cpuset2 = new LongByReference(0); - try { - - final int ret = lib.GetProcessAffinityMask(-1, cpuset1, cpuset2); - // Successful result is positive, according to the docs - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683213%28v=vs.85%29.aspx - if (ret <= 0) - { - throw new IllegalStateException("GetProcessAffinityMask(( -1 ), &(" + cpuset1 + "), &(" + cpuset2 + ") ) return " + ret); - } - - long[] longs = new long[1]; - longs[0] = cpuset1.getValue(); - return BitSet.valueOf(longs); - } - catch (Exception e) - { - LOGGER.error(e.getMessage(), e); - } - - return new BitSet(); + BitSet bitSet = currentAffinity.get(); + if (bitSet != null) + return bitSet; + BitSet longs = getAffinity0(); + return longs != null ? longs : new BitSet(); } @Override @@ -88,8 +57,7 @@ public void setAffinity(final BitSet affinity) { WinDef.DWORD aff; long[] longs = affinity.toLongArray(); - switch (longs.length) - { + switch (longs.length) { case 0: aff = new WinDef.DWORD(0); break; @@ -101,14 +69,44 @@ public void setAffinity(final BitSet affinity) { } int pid = getTid(); - try - { - lib.SetThreadAffinityMask(pid, aff); - } - catch (LastErrorException e) - { + try { + lib.SetThreadAffinityMask(handle(pid), aff); + } catch (LastErrorException e) { throw new IllegalStateException("SetThreadAffinityMask((" + pid + ") , &(" + affinity + ") ) errorNo=" + e.getErrorCode(), e); } + BitSet affinity2 = getAffinity0(); + assert affinity2 != null; + if (!affinity2.intersects(affinity)) { + LoggerFactory.getLogger(WindowsJNAAffinity.class).warn("Tried to set affinity to {} but was {} you may have insufficient access rights", affinity, affinity2); + } + currentAffinity.set((BitSet) affinity.clone()); + } + + @Nullable + private BitSet getAffinity0() { + final CLibrary lib = CLibrary.INSTANCE; + final LongByReference cpuset1 = new LongByReference(0); + final LongByReference cpuset2 = new LongByReference(0); + try { + + final int ret = lib.GetProcessAffinityMask(handle(-1), cpuset1, cpuset2); + // Successful result is positive, according to the docs + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms683213%28v=vs.85%29.aspx + if (ret <= 0) { + throw new IllegalStateException("GetProcessAffinityMask(( -1 ), &(" + cpuset1 + "), &(" + cpuset2 + ") ) return " + ret); + } + + long[] longs = new long[1]; + longs[0] = cpuset1.getValue(); + return BitSet.valueOf(longs); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + return null; + } + + private WinNT.HANDLE handle(int pid) { + return new WinNT.HANDLE(new Pointer(pid)); } public int getTid() { @@ -143,11 +141,11 @@ public int getThreadId() { * @author BegemoT */ private interface CLibrary extends Library { - CLibrary INSTANCE = (CLibrary) Native.loadLibrary("kernel32", CLibrary.class); + CLibrary INSTANCE = Native.load("kernel32", CLibrary.class); - int GetProcessAffinityMask(final int pid, final PointerType lpProcessAffinityMask, final PointerType lpSystemAffinityMask) throws LastErrorException; + int GetProcessAffinityMask(final WinNT.HANDLE pid, final PointerType lpProcessAffinityMask, final PointerType lpSystemAffinityMask) throws LastErrorException; - void SetThreadAffinityMask(final int pid, final WinDef.DWORD lpProcessAffinityMask) throws LastErrorException; + void SetThreadAffinityMask(final WinNT.HANDLE pid, final WinDef.DWORD lpProcessAffinityMask) throws LastErrorException; int GetCurrentThread() throws LastErrorException; } diff --git a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java new file mode 100644 index 000000000..eb2394ddd --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java @@ -0,0 +1,243 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.lockchecker; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.nio.file.NoSuchFileException; +import java.nio.file.OpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import static java.nio.file.StandardOpenOption.*; +import static net.openhft.affinity.impl.VanillaCpuLayout.MAX_CPUS_SUPPORTED; + +public class FileLockBasedLockChecker implements LockChecker { + + private static final int MAX_LOCK_RETRIES = 5; + private static final ThreadLocal dfTL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy.MM" + ".dd 'at' HH:mm:ss z")); + private static final FileAttribute> LOCK_FILE_ATTRIBUTES = PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-rw-rw-")); + private static final Set LOCK_FILE_OPEN_OPTIONS = new HashSet<>(Arrays.asList(READ, WRITE, CREATE, SYNC)); + private static final Logger LOGGER = LoggerFactory.getLogger(FileLockBasedLockChecker.class); + private static final FileLockBasedLockChecker instance = new FileLockBasedLockChecker(); + private final LockReference[] locks = new LockReference[MAX_CPUS_SUPPORTED]; + + protected FileLockBasedLockChecker() { + //nothing + } + + public static LockChecker getInstance() { + return instance; + } + + @Override + public synchronized boolean isLockFree(int id) { + // check if this process already has the lock + if (locks[id] != null) { + return false; + } + + // check if another process has the lock + File lockFile = toFile(id); + try (final 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)) { + 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); + } + return true; + } else { + // another process has an exclusive lock + return false; + } + } catch (OverlappingFileLockException e) { + // someone else (in the same JVM) has an exclusive lock + /* + * This shouldn't happen under normal circumstances, we have the singleton + * {@link #locks} array to prevent overlapping locks from the same JVM, but + * it can occur when there are multiple classloaders in the JVM + */ + return false; + } + } catch (NoSuchFileException e) { + // no lock file exists, nobody has the lock + return true; + } catch (IOException e) { + LOGGER.warn("An unexpected error occurred checking if the lock was free, assuming it's not", e); + return false; + } + } + + @Override + public synchronized boolean obtainLock(int id, int id2, String metaInfo) throws IOException { + int attempt = 0; + while (attempt < MAX_LOCK_RETRIES) { + try { + LockReference lockReference = tryAcquireLockOnFile(id, metaInfo); + if (lockReference != null) { + if (id2 <= 0) { + // no second lock to acquire, return success + locks[id] = lockReference; + return true; + } + LockReference lockReference2 = tryAcquireLockOnFile(id2, metaInfo); + if (lockReference2 != null) { + locks[id] = lockReference; + locks[id2] = lockReference2; + return true; + } else { + releaseLock(id); + } + } + return false; + } catch (ConcurrentLockFileDeletionException e) { + attempt++; + } + } + LOGGER.warn("Exceeded maximum retries for locking CPU {}, {}, failing acquire", id, id2); + return false; + } + + /** + * Attempts to acquire an exclusive lock on the core lock file. + *

+ * It will fail if another process already has an exclusive lock on the file. + * + * @param id The CPU ID to acquire + * @param metaInfo The meta-info to write to the file upon successful acquisition + * @return The {@link LockReference} if the lock was successfully acquired, null otherwise + * @throws IOException If an IOException occurs creating or writing to the file + * @throws ConcurrentLockFileDeletionException If another process deleted the file between us opening it and locking it + */ + private LockReference tryAcquireLockOnFile(int id, String metaInfo) throws IOException, ConcurrentLockFileDeletionException { + final File lockFile = toFile(id); + final FileChannel fileChannel = FileChannel.open(lockFile.toPath(), LOCK_FILE_OPEN_OPTIONS, LOCK_FILE_ATTRIBUTES); // NOSONAR + try { + final FileLock fileLock = fileChannel.tryLock(0, Long.MAX_VALUE, false); + if (fileLock == null) { + // someone else has a lock (exclusive or shared), fail to acquire + closeQuietly(fileChannel); + return null; + } else { + if (!lockFile.exists()) { + // someone deleted the file between us opening it and acquiring the lock, signal to retry + closeQuietly(fileLock, fileChannel); + throw new ConcurrentLockFileDeletionException(); + } else { + // we have the lock, the file exists. That's success. + writeMetaInfoToFile(fileChannel, metaInfo); + return new LockReference(fileChannel, fileLock); + } + } + } catch (OverlappingFileLockException e) { + // someone else (in the same JVM) has a lock, fail to acquire + /* + * This shouldn't happen under normal circumstances, we have the singleton + * {@link #locks} array to prevent overlapping locks from the same JVM, but + * it can occur when there are multiple classloaders in the JVM + */ + closeQuietly(fileChannel); + return null; + } + } + + private void writeMetaInfoToFile(FileChannel fc, String metaInfo) throws IOException { + byte[] content = String.format("%s%n%s", metaInfo, dfTL.get().format(new Date())).getBytes(); + ByteBuffer buffer = ByteBuffer.wrap(content); + while (buffer.hasRemaining()) { + //noinspection ResultOfMethodCallIgnored + fc.write(buffer); + } + } + + @Override + public synchronized boolean releaseLock(int id) { + if (locks[id] != null) { + final File lockFile = toFile(id); + if (!lockFile.delete()) { + LOGGER.warn("Couldn't delete lock file on release: {}", lockFile); + } + closeQuietly(locks[id].lock, locks[id].channel); + locks[id] = null; + return true; + } + return false; + } + + private void closeQuietly(AutoCloseable... closeables) { + for (AutoCloseable closeable : closeables) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (Exception e) { + LOGGER.warn("Error closing {}", closeable.getClass().getName(), e); + } + } + } + + @Override + public String getMetaInfo(int id) throws IOException { + final File file = toFile(id); + + LockReference lr = locks[id]; + if (lr != null) { + return readMetaInfoFromLockFileChannel(file, lr.channel); + } else { + try (FileChannel fc = FileChannel.open(file.toPath(), READ)) { + return readMetaInfoFromLockFileChannel(file, fc); + } catch (NoSuchFileException e) { + return null; + } + } + } + + 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); + if (content.isEmpty()) { + LOGGER.warn("Empty lock file {}", lockFile.getAbsolutePath()); + return null; + } + return content.substring(0, content.indexOf("\n")); + } + + @NotNull + protected File toFile(int id) { + assert id >= 0; + return new File(tmpDir(), "cpu-" + id + ".lock"); + } + + private File tmpDir() { + final File tempDir = new File(System.getProperty("java.io.tmpdir")); + + if (!tempDir.exists()) + tempDir.mkdirs(); + + return tempDir; + } + + /** + * Thrown when another process deleted the lock file between us opening the file and acquiring the lock + */ + static class ConcurrentLockFileDeletionException extends Exception { + private static final long serialVersionUID = 0L; + } +} diff --git a/affinity/src/main/java/net/openhft/affinity/lockchecker/LockChecker.java b/affinity/src/main/java/net/openhft/affinity/lockchecker/LockChecker.java new file mode 100644 index 000000000..ddbe0d49c --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/LockChecker.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.lockchecker; + +import java.io.IOException; + +/** + * @author Tom Shercliff + */ + +public interface LockChecker { + + boolean isLockFree(int id); + + /** + * Obtain a lock for the given id. + */ + @Deprecated(/* to be removed in x.29 */) + default boolean obtainLock(int id, String metaInfo) throws IOException { + return obtainLock(id, 0, metaInfo); + } + + /** + * Obtain a lock for the given id and id2. The id2 is used to distinguish between + * multiple locks for the same core + */ + boolean obtainLock(int id, int id2, String metaInfo) throws IOException; + + boolean releaseLock(int id); + + String getMetaInfo(int id) throws IOException; +} diff --git a/affinity/src/main/java/net/openhft/affinity/lockchecker/LockReference.java b/affinity/src/main/java/net/openhft/affinity/lockchecker/LockReference.java new file mode 100644 index 000000000..10e2b1bdf --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/LockReference.java @@ -0,0 +1,25 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.lockchecker; + +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; + +/** + * @author Tom Shercliff + */ + +public class LockReference { + protected final FileChannel channel; + protected final FileLock lock; + + public LockReference(final FileChannel channel, final FileLock lock) { + this.channel = channel; + this.lock = lock; + } + + public FileChannel getChannel() { + return channel; + } +} diff --git a/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java b/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java new file mode 100644 index 000000000..fcaa3281a --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +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 { + + public static void main(String[] args) { + + int cpus; + if (args.length == 0) { + cpus = AffinityLock.cpuLayout().cpus() / 12; + } else { + cpus = Integer.parseInt(args[0]); + } + + for (int i = 0; i < cpus; i++) { + acquireAndDoWork(); + } + } + + 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(); + } +} diff --git a/affinity/src/main/java/net/openhft/ticker/ITicker.java b/affinity/src/main/java/net/openhft/ticker/ITicker.java index 6e04f4645..3a9ab33d3 100644 --- a/affinity/src/main/java/net/openhft/ticker/ITicker.java +++ b/affinity/src/main/java/net/openhft/ticker/ITicker.java @@ -1,23 +1,25 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.ticker; /** - * Created by peter on 13/07/15. + * Abstraction of a high resolution time source used throughout the library. + *

+ * Implementations may be based on {@link System#nanoTime()} or platform + * specific timers such as the processor's time stamp counter accessed via + * JNI. The {@linkplain #ticks() tick values} returned are therefore + * implementation dependent. They always increase monotonically but the unit + * they represent can vary from nanoseconds to CPU cycles. + *

+ * Utility methods are provided to convert these raw ticks into conventional + * time units. For example {@link #toNanos(long)} converts the supplied number + * of ticks to nanoseconds and {@link #toMicros(double)} converts them to + * microseconds. + *

+ * This interface is typically accessed via the {@link net.openhft.ticker.Ticker} + * helper class which selects the best available implementation for the + * running platform. */ public interface ITicker { long nanoTime(); diff --git a/affinity/src/main/java/net/openhft/ticker/Ticker.java b/affinity/src/main/java/net/openhft/ticker/Ticker.java index 9fffdaafe..4d057cf2a 100644 --- a/affinity/src/main/java/net/openhft/ticker/Ticker.java +++ b/affinity/src/main/java/net/openhft/ticker/Ticker.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.ticker; import net.openhft.ticker.impl.JNIClock; @@ -35,6 +22,10 @@ public final class Ticker { } } + private Ticker() { + throw new InstantiationError("Must not instantiate this class"); + } + /** * @return The current value of the system timer, in nanoseconds. */ @@ -53,4 +44,4 @@ public static long toNanos(long ticks) { public static double toMicros(long ticks) { return INSTANCE.toMicros(ticks); } -} \ No newline at end of file +} 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 e3bd78439..c50c3826c 100644 --- a/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java +++ b/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.ticker.impl; import net.openhft.ticker.ITicker; @@ -64,13 +51,14 @@ static long tscToNano(final long tsc) { return (tsc * RDTSC_FACTOR) >> FACTOR_BITS; } + @SuppressWarnings("StatementWithEmptyBody") private static void estimateFrequency(int factor) { final long start = System.nanoTime(); long now; - while ((now = System.nanoTime()) == start) { + while (System.nanoTime() == start) { } - long end = start + factor * 1000000; + long end = start + factor * 1000000L; final long start0 = rdtsc0(); while ((now = System.nanoTime()) < end) { } @@ -102,4 +90,4 @@ public long toNanos(long ticks) { public double toMicros(double ticks) { return ticks * RDTSC_MICRO_FACTOR; } -} \ No newline at end of file +} diff --git a/affinity/src/main/java/net/openhft/ticker/impl/SystemClock.java b/affinity/src/main/java/net/openhft/ticker/impl/SystemClock.java index 1d7e5d445..cbbd3545e 100644 --- a/affinity/src/main/java/net/openhft/ticker/impl/SystemClock.java +++ b/affinity/src/main/java/net/openhft/ticker/impl/SystemClock.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.ticker.impl; import net.openhft.ticker.ITicker; @@ -46,5 +33,4 @@ public long toNanos(long ticks) { public double toMicros(double ticks) { return ticks / 1e3; } - -} \ No newline at end of file +} 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 2e1212f19..aa501a8f0 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 @@ -1,3 +1,6 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ package software.chronicle.enterprise.internals.impl; import net.openhft.affinity.IAffinity; @@ -25,6 +28,7 @@ public enum NativeAffinity implements IAffinity { private native static long rdtsc0(); + @SuppressWarnings("restricted") private static boolean loadAffinityNativeLibrary() { try { System.loadLibrary("CEInternals"); @@ -35,19 +39,16 @@ private static boolean loadAffinityNativeLibrary() { } @Override - public BitSet getAffinity() - { + public BitSet getAffinity() { final byte[] buff = getAffinity0(); - if (buff == null) - { + if (buff == null) { return null; } return BitSet.valueOf(buff); } @Override - public void setAffinity(BitSet affinity) - { + public void setAffinity(BitSet affinity) { setAffinity0(affinity.toByteArray()); } diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java b/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java index 2d2fe87de..b1ed7effe 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; import static net.openhft.affinity.AffinityStrategies.*; @@ -21,7 +8,11 @@ /** * @author peter.lawrey */ -public class AffinityLockBindMain { +public final class AffinityLockBindMain { + private AffinityLockBindMain() { + throw new InstantiationError("Must not instantiate this class"); + } + public static void main(String... args) throws InterruptedException { AffinityLock al = AffinityLock.acquireLock(); try { diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityLockDumpLocksTest.java b/affinity/src/test/java/net/openhft/affinity/AffinityLockDumpLocksTest.java new file mode 100644 index 000000000..dd87e4f4e --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockDumpLocksTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import net.openhft.affinity.impl.VanillaCpuLayout; +import org.junit.Assume; +import org.junit.Test; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; + +public class AffinityLockDumpLocksTest extends BaseAffinityTest { + + private static void supressUnusedWarning(AutoCloseable c) { + // do nothing + } + + @Test + public void dumpLocksListsThreadsHoldingLocks() throws Exception { + Assume.assumeTrue(new File("/proc/cpuinfo").exists()); + + AffinityLock.cpuLayout(VanillaCpuLayout.fromCpuInfo()); + int nThreads = Math.min(3, Math.max(1, AffinityLock.PROCESSORS - 1)); + CountDownLatch acquired = new CountDownLatch(nThreads); + CountDownLatch release = new CountDownLatch(1); + List threads = new ArrayList<>(); + + for (int i = 0; i < nThreads; i++) { + String name = "worker-" + i; + Thread t = new Thread(() -> { + try (AffinityLock lock = AffinityLock.acquireLock()) { + supressUnusedWarning(lock); + acquired.countDown(); + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }, name); + threads.add(t); + t.start(); + } + + assertTrue("threads failed to acquire locks", acquired.await(5, TimeUnit.SECONDS)); + + String dump = AffinityLock.dumpLocks(); + for (Thread t : threads) { + assertTrue("Missing entry for " + t.getName(), dump.contains(t.getName())); + } + + release.countDown(); + for (Thread t : threads) { + t.join(); + } + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityLockMain.java b/affinity/src/test/java/net/openhft/affinity/AffinityLockMain.java index 92bf70a44..cf7388ec6 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityLockMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockMain.java @@ -1,25 +1,16 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; /** * @author peter.lawrey */ -public class AffinityLockMain { +public final class AffinityLockMain { + private AffinityLockMain() { + throw new InstantiationError("Must not instantiate this class"); + } + public static void main(String... args) throws InterruptedException { AffinityLock al = AffinityLock.acquireLock(); try { diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityLockReleaseTest.java b/affinity/src/test/java/net/openhft/affinity/AffinityLockReleaseTest.java new file mode 100644 index 000000000..dc964296f --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockReleaseTest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import net.openhft.affinity.impl.VanillaCpuLayout; +import org.junit.Test; + +import java.io.File; + +import static org.junit.Assert.assertEquals; + +/** + * Unit test to verify that releasing an {@link AffinityLock} restores the + * affinity mask back to {@link AffinityLock#BASE_AFFINITY}. + */ +public class AffinityLockReleaseTest extends BaseAffinityTest { + + @Test + public void acquireAndReleaseShouldRestoreBaseAffinity() throws Exception { + if (!new File("/proc/cpuinfo").exists()) { + System.out.println("Cannot run affinity test as this system doesn't have a /proc/cpuinfo file"); + return; + } + + // initialise CPU layout from the running machine so acquireLock works + AffinityLock.cpuLayout(VanillaCpuLayout.fromCpuInfo()); + + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); + AffinityLock lock = AffinityLock.acquireLock(); + assertEquals(1, Affinity.getAffinity().cardinality()); + lock.release(); + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java b/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java index 7d6458ab1..4e8ff5519 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java @@ -1,52 +1,57 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; +import net.openhft.affinity.impl.Utilities; import net.openhft.affinity.impl.VanillaCpuLayout; +import net.openhft.chronicle.testframework.Waiters; +import org.hamcrest.MatcherAssert; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.BitSet; import java.util.List; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotSame; +import static net.openhft.affinity.AffinityLock.PROCESSORS; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeTrue; /** * @author peter.lawrey */ -@SuppressWarnings("ALL") -public class AffinityLockTest { +public class AffinityLockTest extends BaseAffinityTest { private static final Logger logger = LoggerFactory.getLogger(AffinityLockTest.class); + + /** + * In Java 21 the toString contents of Thread changed to include an ID. This breaks the tests here in Java 21. + * Strip out the thread ID here so that existing tests continue to pass. + */ + private static String dumpLocks(AffinityLock[] locks) { + String value = LockInventory.dumpLocks(locks); + return value.replaceAll("#[0-9]+(,)?", ""); + } + @Test public void dumpLocksI7() throws IOException { LockInventory lockInventory = new LockInventory(VanillaCpuLayout.fromCpuInfo("i7.cpuinfo")); AffinityLock[] locks = { - new AffinityLock(0, true, false, lockInventory), - new AffinityLock(1, false, false, lockInventory), - new AffinityLock(2, false, true, lockInventory), - new AffinityLock(3, false, true, lockInventory), - new AffinityLock(4, true, false, lockInventory), - new AffinityLock(5, false, false, lockInventory), - new AffinityLock(6, false, true, lockInventory), - new AffinityLock(7, false, true, lockInventory), + new AffinityLock(0, 0, true, false, lockInventory), + new AffinityLock(1, 5, false, false, lockInventory), + new AffinityLock(2, 6, false, true, lockInventory), + new AffinityLock(3, 7, false, true, lockInventory), + new AffinityLock(4, 0, true, false, lockInventory), + new AffinityLock(5, 1, false, false, lockInventory), + new AffinityLock(6, 2, false, true, lockInventory), + new AffinityLock(7, 3, false, true, lockInventory), }; locks[2].assignedThread = new Thread(new InterrupedThread(), "logger"); locks[2].assignedThread.start(); @@ -55,7 +60,7 @@ public void dumpLocksI7() throws IOException { locks[6].assignedThread = new Thread(new InterrupedThread(), "main"); locks[7].assignedThread = new Thread(new InterrupedThread(), "tcp"); locks[7].assignedThread.start(); - final String actual = LockInventory.dumpLocks(locks); + final String actual = dumpLocks(locks); assertEquals("0: General use CPU\n" + "1: CPU not available\n" + "2: Thread[logger,5,main] alive=true\n" + @@ -76,16 +81,16 @@ public void dumpLocksI7() throws IOException { public void dumpLocksI3() throws IOException { LockInventory lockInventory = new LockInventory(VanillaCpuLayout.fromCpuInfo("i3.cpuinfo")); AffinityLock[] locks = { - new AffinityLock(0, true, false, lockInventory), - new AffinityLock(1, false, true, lockInventory), - new AffinityLock(2, true, false, lockInventory), - new AffinityLock(3, false, true, lockInventory), + new AffinityLock(0, 0, true, false, lockInventory), + new AffinityLock(1, 3, false, true, lockInventory), + new AffinityLock(2, 0, true, false, lockInventory), + new AffinityLock(3, 1, false, true, lockInventory), }; locks[1].assignedThread = new Thread(new InterrupedThread(), "engine"); locks[1].assignedThread.start(); locks[3].assignedThread = new Thread(new InterrupedThread(), "main"); - final String actual = LockInventory.dumpLocks(locks); + final String actual = dumpLocks(locks); assertEquals("0: General use CPU\n" + "1: Thread[engine,5,main] alive=true\n" + "2: General use CPU\n" + @@ -99,13 +104,13 @@ public void dumpLocksI3() throws IOException { public void dumpLocksCoreDuo() throws IOException { LockInventory lockInventory = new LockInventory(VanillaCpuLayout.fromCpuInfo("core.duo.cpuinfo")); AffinityLock[] locks = { - new AffinityLock(0, true, false, lockInventory), - new AffinityLock(1, false, true, lockInventory), + new AffinityLock(0, 0, true, false, lockInventory), + new AffinityLock(1, 0, false, true, lockInventory), }; locks[1].assignedThread = new Thread(new InterrupedThread(), "engine"); locks[1].assignedThread.start(); - final String actual = LockInventory.dumpLocks(locks); + final String actual = dumpLocks(locks); assertEquals("0: General use CPU\n" + "1: Thread[engine,5,main] alive=true\n", actual); System.out.println(actual); @@ -124,6 +129,7 @@ public void assignReleaseThread() throws IOException { System.out.println("Cannot run affinity test as this system doesn't have a /proc/cpuinfo file"); return; } + AffinityLock.cpuLayout(VanillaCpuLayout.fromCpuInfo()); assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); @@ -139,6 +145,22 @@ public void assignReleaseThread() throws IOException { assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); } + @Test + public void resetAffinity() { + assumeTrue(System.getProperty("os.name").contains("nux")); + assertTrue(Affinity.getAffinity().cardinality() > 1); + try (AffinityLock lock = AffinityLock.acquireLock()) { + assertEquals(1, Affinity.getAffinity().cardinality()); + assertTrue(lock.resetAffinity()); + lock.resetAffinity(false); + } + assertEquals(1, Affinity.getAffinity().cardinality()); + try (AffinityLock lock = AffinityLock.acquireLock()) { + assertNotNull(lock); + } + assertTrue(Affinity.getAffinity().cardinality() > 1); + } + @Test public void testIssue21() throws IOException { if (!new File("/proc/cpuinfo").exists()) { @@ -151,13 +173,15 @@ public void testIssue21() throws IOException { if (Runtime.getRuntime().availableProcessors() > 2) { AffinityLock alForAnotherThread2 = al.acquireLock(AffinityStrategies.ANY); assertNotSame(alForAnotherThread, alForAnotherThread2); - assertNotSame(alForAnotherThread.cpuId(), alForAnotherThread2.cpuId()); + if (alForAnotherThread.cpuId() != -1) + assertNotSame(alForAnotherThread.cpuId(), alForAnotherThread2.cpuId()); alForAnotherThread2.release(); } else { assertNotSame(alForAnotherThread, al); - assertNotSame(alForAnotherThread.cpuId(), al.cpuId()); + if (alForAnotherThread.cpuId() != -1) + assertNotSame(alForAnotherThread.cpuId(), al.cpuId()); } alForAnotherThread.release(); al.release(); @@ -165,10 +189,10 @@ public void testIssue21() throws IOException { @Test public void testIssue19() { - System.out.println("AffinityLock.PROCESSORS=" + AffinityLock.PROCESSORS); + System.out.println("AffinityLock.PROCESSORS=" + PROCESSORS); AffinityLock al = AffinityLock.acquireLock(); - List locks = new ArrayList(); + List locks = new ArrayList<>(); locks.add(al); for (int i = 0; i < 256; i++) locks.add(al = al.acquireLock(AffinityStrategies.DIFFERENT_SOCKET, @@ -187,34 +211,160 @@ public void testGettid() { @Test public void testAffinity() throws InterruptedException { - // System.out.println("Started"); logger.info("Started"); displayStatus(); - final AffinityLock al = AffinityLock.acquireLock(); - try { + try (AffinityLock al = AffinityLock.acquireLock()) { System.out.println("Main locked"); displayStatus(); - Thread t = new Thread(new Runnable() { - @Override - public void run() { - AffinityLock al2 = al.acquireLock(AffinityStrategies.SAME_SOCKET, AffinityStrategies.ANY); - System.out.println("Thread-0 locked"); - displayStatus(); - al2.release(); - } + Thread t = new Thread(() -> { + AffinityLock al2 = al.acquireLock(AffinityStrategies.SAME_SOCKET, AffinityStrategies.ANY); + System.out.println("Thread-0 locked"); + displayStatus(); + al2.release(); }); t.start(); t.join(); System.out.println("Thread-0 unlocked"); displayStatus(); - } finally { - al.close(); } System.out.println("All unlocked"); displayStatus(); } + @Test + public void shouldReturnLockForSpecifiedCpu() { + assumeTrue(Runtime.getRuntime().availableProcessors() > 3); + + try (final AffinityLock affinityLock = AffinityLock.acquireLock(3)) { + MatcherAssert.assertThat(affinityLock.cpuId(), is(3)); + } + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); + } + + @Test + public void lockFilesShouldBeRemovedOnRelease() { + if (!Utilities.ISLINUX) { + return; + } + final AffinityLock lock = AffinityLock.acquireLock(); + + Path lockFile = Paths.get(System.getProperty("java.io.tmpdir"), "cpu-" + lock.cpuId() + ".lock"); + assertTrue(Files.exists(lockFile)); + + lock.release(); + + assertFalse(Files.exists(lockFile)); + } + + @Test + public void wholeCoreLockReservesAllLogicalCpus() throws IOException { + if (!Utilities.ISLINUX || !new File("/proc/cpuinfo").exists()) { + return; + } + AffinityLock.cpuLayout(VanillaCpuLayout.fromCpuInfo()); + + CpuLayout layout = AffinityLock.cpuLayout(); + try (AffinityLock lock = AffinityLock.acquireCore()) { + int socketId = layout.socketId(lock.cpuId()); + int coreId = layout.coreId(lock.cpuId()); + for (int i = 0; i < layout.cpus(); i++) { + if (layout.socketId(i) == socketId && layout.coreId(i) == coreId) { + assertFalse("CPU " + i + " should be reserved", LockCheck.isCpuFree(i)); + } + } + } + for (int i = 0; i < layout.cpus(); i++) { + assertTrue("CPU " + i + " should not be reserved", LockCheck.isCpuFree(i)); + } + } + private void displayStatus() { System.out.println(Thread.currentThread() + " on " + Affinity.getCpu() + "\n" + AffinityLock.dumpLocks()); } + + @Test + public void testAffinityLockDescriptions() { + if (!Utilities.ISLINUX) { + return; + } + try (AffinityLock lock = AffinityLock.acquireLock("last")) { + assertNotNull(lock); + assertEquals(PROCESSORS - 1, Affinity.getCpu()); + } + try (AffinityLock lock = AffinityLock.acquireLock("last")) { + assertNotNull(lock); + assertEquals(PROCESSORS - 1, Affinity.getCpu()); + } + try (AffinityLock lock = AffinityLock.acquireLock("last-1")) { + assertNotNull(lock); + assertEquals(PROCESSORS - 2, Affinity.getCpu()); + } + try (AffinityLock lock = AffinityLock.acquireLock("1")) { + assertNotNull(lock); + assertEquals(1, Affinity.getCpu()); + } + try (AffinityLock lock = AffinityLock.acquireLock("any")) { + assertTrue(lock.bound); + } + try (AffinityLock lock = AffinityLock.acquireLock("none")) { + assertFalse(lock.bound); + } + try (AffinityLock lock = AffinityLock.acquireLock((String) null)) { + assertFalse(lock.bound); + } + try (AffinityLock lock = AffinityLock.acquireLock("0")) { + assertFalse(lock.bound); + } + } + + @Test + public void acquireLockWithoutBindingDoesNotChangeAffinity() { + BitSet before = (BitSet) Affinity.getAffinity().clone(); + try (AffinityLock lock = AffinityLock.acquireLock(false)) { + assertFalse(lock.isBound()); + assertEquals(before, Affinity.getAffinity()); + } + assertEquals(before, Affinity.getAffinity()); + } + + @Test + public void testTooHighCpuId() { + assertFalse(AffinityLock.acquireLock(123456).isBound()); + } + + @Test + public void testNegativeCpuId() { + assertFalse(AffinityLock.acquireLock(-1).isBound()); + } + + @Test + public void testTooHighCpuId2() { + AffinityLock lock = AffinityLock.acquireLock(new int[]{123456}); + assertFalse(lock.isBound()); + } + + @Test(expected = IllegalStateException.class) + public void bindingTwoThreadsToSameCpuThrows() throws InterruptedException { + assumeTrue(Runtime.getRuntime().availableProcessors() > 1); + + final AffinityLock lock = AffinityLock.acquireLock(false); + Thread t = new Thread(() -> { + lock.bind(); + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + // ignored + } + }); + t.start(); + + Waiters.waitForCondition("Waiting for lock to be bound", lock::isBound, 1000); + + try { + lock.bind(); + } finally { + t.join(); + lock.release(); + } + } } diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityResetToBaseAffinityTest.java b/affinity/src/test/java/net/openhft/affinity/AffinityResetToBaseAffinityTest.java new file mode 100644 index 000000000..86ad17fed --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityResetToBaseAffinityTest.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import net.openhft.affinity.impl.VanillaCpuLayout; +import org.junit.Test; + +import java.io.File; + +import static org.junit.Assert.assertEquals; + +public class AffinityResetToBaseAffinityTest extends BaseAffinityTest { + + @Test + public void resettingShouldRestoreBaseAffinity() throws Exception { + if (!new File("/proc/cpuinfo").exists()) { + System.out.println("Cannot run affinity test as this system doesn't have a /proc/cpuinfo file"); + return; + } + + // initialise CPU layout from the running machine so acquireLock works + AffinityLock.cpuLayout(VanillaCpuLayout.fromCpuInfo()); + + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); + AffinityLock lock = AffinityLock.acquireLock(); + try { + assertEquals(1, Affinity.getAffinity().cardinality()); + + Affinity.resetToBaseAffinity(); + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); + } finally { + lock.release(); + } + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/AffinitySupportMain.java b/affinity/src/test/java/net/openhft/affinity/AffinitySupportMain.java index 9cf4e6710..d7cf5b35d 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinitySupportMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinitySupportMain.java @@ -1,25 +1,16 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; /** * @author peter.lawrey */ -public class AffinitySupportMain { +public final class AffinitySupportMain { + private AffinitySupportMain() { + throw new InstantiationError("Must not instantiate this class"); + } + public static void main(String... args) { AffinityLock al = AffinityLock.acquireLock(); try { diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java index 8a8a3e87a..3dbd15396 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; import java.util.concurrent.Callable; @@ -26,22 +13,24 @@ /** * @author peter.lawrey */ -public class AffinityThreadFactoryMain { +public final class AffinityThreadFactoryMain { private static final ExecutorService ES = Executors.newFixedThreadPool(4, new AffinityThreadFactory("bg", SAME_CORE, DIFFERENT_SOCKET, ANY)); + private AffinityThreadFactoryMain() { + throw new InstantiationError("Must not instantiate this class"); + } + public static void main(String... args) throws InterruptedException { for (int i = 0; i < 12; i++) - ES.submit(new Callable() { - @Override - public Void call() throws InterruptedException { - Thread.sleep(100); - return null; - } + ES.submit((Callable) () -> { + Thread.sleep(100); + return null; }); Thread.sleep(200); System.out.println("\nThe assignment of CPUs is\n" + AffinityLock.dumpLocks()); ES.shutdown(); + //noinspection ResultOfMethodCallIgnored ES.awaitTermination(1, TimeUnit.SECONDS); } } diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryTest.java b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryTest.java new file mode 100644 index 000000000..4c95b8bd4 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.util.Set; +import java.util.concurrent.*; + +import static org.junit.Assert.*; + +public class AffinityThreadFactoryTest extends BaseAffinityTest { + + @Before + public void checkLinux() { + Assume.assumeTrue(LockCheck.IS_LINUX); + } + + @Test + public void threadsReceiveDistinctCpus() throws InterruptedException { + int available = Math.max(1, AffinityLock.PROCESSORS - 1); + int nThreads = Math.min(4, available); + + ExecutorService es = Executors.newFixedThreadPool(nThreads, + new AffinityThreadFactory("test")); + + Set cpus = ConcurrentHashMap.newKeySet(); + CountDownLatch ready = new CountDownLatch(nThreads); + CountDownLatch finished = new CountDownLatch(nThreads); + + for (int i = 0; i < nThreads; i++) { + es.submit(() -> { + cpus.add(Affinity.getCpu()); + ready.countDown(); + try { + ready.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + finished.countDown(); + }); + } + + assertTrue(finished.await(5, TimeUnit.SECONDS)); + es.shutdown(); + es.awaitTermination(5, TimeUnit.SECONDS); + + assertFalse(cpus.contains(-1)); + assertEquals(nThreads, cpus.size()); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/BaseAffinityTest.java b/affinity/src/test/java/net/openhft/affinity/BaseAffinityTest.java new file mode 100644 index 000000000..f147ce33b --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/BaseAffinityTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +import java.util.BitSet; + +import static org.junit.Assert.assertEquals; + +public class BaseAffinityTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + private String originalTmpDir; + + @Before + public void setTmpDirectory() { + originalTmpDir = System.getProperty("java.io.tmpdir"); + System.setProperty("java.io.tmpdir", folder.getRoot().getAbsolutePath()); + } + + @After + public void restoreTmpDirectoryAndReleaseAllLocks() { + // don't leave any locks locked + for (int i = 0; i < AffinityLock.PROCESSORS; i++) { + LockCheck.releaseLock(i); + } + System.setProperty("java.io.tmpdir", originalTmpDir); + } + + @After + public void baseAffinity() { + BitSet affinity = Affinity.getAffinity(); + Affinity.resetToBaseAffinity(); + assertEquals(AffinityLock.BASE_AFFINITY, affinity); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/BootClassPathTest.java b/affinity/src/test/java/net/openhft/affinity/BootClassPathTest.java new file mode 100644 index 000000000..17558a01e --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/BootClassPathTest.java @@ -0,0 +1,16 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class BootClassPathTest extends BaseAffinityTest { + @Test + public void shouldDetectClassesOnClassPath() { + assertTrue(BootClassPath.INSTANCE.has("java.lang.Thread")); + assertTrue(BootClassPath.INSTANCE.has("java.lang.Runtime")); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/FileLockLockCheckTest.java b/affinity/src/test/java/net/openhft/affinity/FileLockLockCheckTest.java new file mode 100644 index 000000000..f1a88ddc1 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/FileLockLockCheckTest.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import net.openhft.affinity.testimpl.TestFileLockBasedLockChecker; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +import static net.openhft.affinity.LockCheck.IS_LINUX; + +/** + * @author Tom Shercliff + */ +public class FileLockLockCheckTest extends BaseAffinityTest { + + private final TestFileLockBasedLockChecker lockChecker = new TestFileLockBasedLockChecker(); + private int cpu = 5; + + @Before + public void before() { + Assume.assumeTrue(IS_LINUX); + } + + @Test + public void test() throws IOException { + Assert.assertTrue(LockCheck.isCpuFree(cpu)); + LockCheck.updateCpu(cpu, 0); + Assert.assertEquals(LockCheck.getPID(), LockCheck.getProcessForCpu(cpu)); + } + + @Test + public void testPidOnLinux() { + Assert.assertTrue(LockCheck.isProcessRunning(LockCheck.getPID())); + } + + @Test + public void testReplace() throws IOException { + cpu++; + Assert.assertTrue(LockCheck.isCpuFree(cpu + 1)); + LockCheck.replacePid(cpu, 0, 123L); + Assert.assertEquals(123L, LockCheck.getProcessForCpu(cpu)); + } + + @Test + public void shouldNotBlowUpIfPidFileIsEmpty() throws Exception { + LockCheck.updateCpu(cpu, 0); + + final File file = lockChecker.doToFile(cpu); + new RandomAccessFile(file, "rw").setLength(0); + + LockCheck.isCpuFree(cpu); + } + + @Test + public void lockFileDeletedWhileHeld() throws Exception { + cpu++; + + Assert.assertTrue(LockCheck.isCpuFree(cpu)); + LockCheck.updateCpu(cpu, 0); + + File lockFile = lockChecker.doToFile(cpu); + Assert.assertTrue(lockFile.exists()); + + Assert.assertTrue("Could not delete lock file", lockFile.delete()); + Assert.assertFalse(lockFile.exists()); + + Assert.assertFalse("CPU should remain locked despite missing file", LockCheck.isCpuFree(cpu)); + Assert.assertEquals(LockCheck.getPID(), LockCheck.getProcessForCpu(cpu)); + + LockCheck.releaseLock(cpu); + + Assert.assertTrue("Lock should be free after release", LockCheck.isCpuFree(cpu)); + LockCheck.updateCpu(cpu, 0); + + lockFile = lockChecker.doToFile(cpu); + Assert.assertTrue("Lock file should be recreated", lockFile.exists()); + } + + @Test + public void getProcessForCpuReturnsEmptyPidWhenNoFile() throws IOException { + int freeCpu = 99; + File lockFile = lockChecker.doToFile(freeCpu); + Assert.assertFalse(lockFile.exists()); + Assert.assertEquals(Integer.MIN_VALUE, LockCheck.getProcessForCpu(freeCpu)); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/InterrupedThread.java b/affinity/src/test/java/net/openhft/affinity/InterrupedThread.java index ab3701f3a..9e85c0790 100644 --- a/affinity/src/test/java/net/openhft/affinity/InterrupedThread.java +++ b/affinity/src/test/java/net/openhft/affinity/InterrupedThread.java @@ -1,19 +1,6 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity; /** diff --git a/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java b/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java new file mode 100644 index 000000000..3f19a4a2f --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import net.openhft.affinity.testimpl.TestFileLockBasedLockChecker; +import org.junit.Assert; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.RandomAccessFile; + +import static net.openhft.affinity.LockCheck.IS_LINUX; + +/** + * @author Rob Austin. + */ +public class LockCheckTest extends BaseAffinityTest { + + private final TestFileLockBasedLockChecker lockChecker = new TestFileLockBasedLockChecker(); + private int cpu = 11; + + @Before + public void before() { + Assume.assumeTrue(IS_LINUX); + } + + @Test + public void test() throws IOException { + Assert.assertTrue(LockCheck.isCpuFree(cpu)); + LockCheck.updateCpu(cpu, 0); + Assert.assertEquals(LockCheck.getPID(), LockCheck.getProcessForCpu(cpu)); + } + + @Test + public void testPidOnLinux() { + Assert.assertTrue(LockCheck.isProcessRunning(LockCheck.getPID())); + } + + @Test + public void testNegativePidOnLinux() { + Assert.assertFalse(LockCheck.isProcessRunning(-1)); + } + + @Test + public void testReplace() throws IOException { + cpu++; + Assert.assertTrue(LockCheck.isCpuFree(cpu + 1)); + LockCheck.replacePid(cpu, 0, 123L); + Assert.assertEquals(123L, LockCheck.getProcessForCpu(cpu)); + } + + @Test + public void shouldNotBlowUpIfPidFileIsEmpty() throws Exception { + LockCheck.updateCpu(cpu, 0); + + final File file = lockChecker.doToFile(cpu); + new RandomAccessFile(file, "rw").setLength(0); + + LockCheck.isCpuFree(cpu); + } + + @Test + public void shouldNotBlowUpIfPidFileIsCorrupt() throws Exception { + LockCheck.updateCpu(cpu, 0); + + final File file = lockChecker.doToFile(cpu); + try (final FileWriter writer = new FileWriter(file, false)) { + writer.append("not a number\nnot a date"); + } + + LockCheck.isCpuFree(cpu); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java b/affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java new file mode 100644 index 000000000..63dd9105b --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java @@ -0,0 +1,262 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity; + +import net.openhft.affinity.testimpl.TestFileLockBasedLockChecker; +import net.openhft.chronicle.testframework.process.JavaProcessBuilder; +import org.jetbrains.annotations.NotNull; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static java.lang.String.format; +import static net.openhft.affinity.LockCheck.IS_LINUX; +import static org.junit.Assert.*; + +public class MultiProcessAffinityTest extends BaseAffinityTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(MultiProcessAffinityTest.class); + + @Before + public void setUp() { + Assume.assumeTrue(IS_LINUX); + } + + @Test + public void shouldNotAcquireLockOnCoresLockedByOtherProcesses() throws InterruptedException { + // run the separate affinity locker + final Process affinityLockerProcess = JavaProcessBuilder.create(AffinityLockerProcess.class) + .withJvmArguments("-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()) + .withProgramArguments("last").start(); + try { + int lastCpuId = AffinityLock.PROCESSORS - 1; + + // wait for the CPU to be locked + long endTime = System.currentTimeMillis() + 5_000; + while (LockCheck.isCpuFree(lastCpuId)) { + //noinspection BusyWait + Thread.sleep(100); + if (System.currentTimeMillis() > endTime) { + LOGGER.info("Timed out waiting for the lock to be acquired: isAlive={}, exitCode={}", + affinityLockerProcess.isAlive(), affinityLockerProcess.isAlive() ? "N/A" : affinityLockerProcess.exitValue()); + JavaProcessBuilder.printProcessOutput("AffinityLockerProcess", affinityLockerProcess); + fail("Timed out waiting for the sub-process to acquire the lock"); + } + } + + try (AffinityLock lock = AffinityLock.acquireLock("last")) { + assertNotEquals(lastCpuId, lock.cpuId()); + } + } finally { + affinityLockerProcess.destroy(); + waitForProcessToEnd(5, "Affinity locker process", affinityLockerProcess, false); + } + } + + @Test + public void shouldAllocateCoresCorrectlyUnderContention() throws InterruptedException { + final int numberOfLockers = Math.max(2, Math.min(12, Runtime.getRuntime().availableProcessors())) / 2; + List lockers = new ArrayList<>(); + LOGGER.info("Running test with {} locker processes", numberOfLockers); + for (int i = 0; i < numberOfLockers; i++) { + lockers.add(JavaProcessBuilder.create(RepeatedAffinityLocker.class) + .withJvmArguments("-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()) + .withProgramArguments("last", "30", "2").start()); + } + for (int i = 0; i < numberOfLockers; i++) { + final Process process = lockers.get(i); + waitForProcessToEnd(20, "Locking process", process); + } + } + + @Test + public void shouldAllocateCoresCorrectlyUnderContentionWithFailures() throws InterruptedException { + final int numberOfLockers = Math.max(2, Math.min(12, Runtime.getRuntime().availableProcessors())) / 2; + List lockers = new ArrayList<>(); + LOGGER.info("Running test with {} locker processes", numberOfLockers); + Process lockFileDropper = JavaProcessBuilder.create(LockFileDropper.class).start(); + for (int i = 0; i < numberOfLockers; i++) { + lockers.add(JavaProcessBuilder.create(RepeatedAffinityLocker.class) + .withJvmArguments("-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()) + .withProgramArguments("last", "30", "2").start()); + } + for (int i = 0; i < numberOfLockers; i++) { + final Process process = lockers.get(i); + waitForProcessToEnd(20, "Locking process", process); + } + lockFileDropper.destroy(); + waitForProcessToEnd(5, "Lock file droppper", lockFileDropper); + } + + @Test + public void shouldBeAbleToAcquireLockLeftByOtherProcess() throws InterruptedException { + final Process process = JavaProcessBuilder.create(AffinityLockerThatDoesNotReleaseProcess.class) + .withJvmArguments("-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()) + .withProgramArguments("last").start(); + waitForProcessToEnd(5, "Locking process", process); + // We should be able to acquire the lock despite the other process not explicitly releasing it + try (final AffinityLock acquired = AffinityLock.acquireLock("last")) { + assertEquals(AffinityLock.PROCESSORS - 1, acquired.cpuId()); + } + } + + private void waitForProcessToEnd(int timeToWaitSeconds, String processDescription, Process process) throws InterruptedException { + waitForProcessToEnd(timeToWaitSeconds, processDescription, process, true); + } + + private void waitForProcessToEnd(int timeToWaitSeconds, String processDescription, Process process, boolean checkExitCode) throws InterruptedException { + if (!process.waitFor(timeToWaitSeconds, TimeUnit.SECONDS)) { + JavaProcessBuilder.printProcessOutput(processDescription, process); + fail(processDescription + " didn't end in time"); + } + if (checkExitCode && process.exitValue() != 0) { + JavaProcessBuilder.printProcessOutput(processDescription, process); + fail(processDescription + " failed, see output above (exit value " + process.exitValue() + ")"); + } + } + + /** + * Repeatedly acquires and releases a lock on the specified core + */ + static class RepeatedAffinityLocker implements Callable { + + private static final Logger LOGGER = LoggerFactory.getLogger(RepeatedAffinityLocker.class); + private static final long PID = LockCheck.getPID(); + private final int iterations; + private final String cpuIdToLock; + + RepeatedAffinityLocker(String cpuIdToLock, int iterations) { + this.iterations = iterations; + this.cpuIdToLock = cpuIdToLock; + } + + public static void main(String[] args) throws InterruptedException, ExecutionException { + String cpuIdToLock = args[0]; + int iterations = Integer.parseInt(args[1]); + int threads = Integer.parseInt(args[2]); + + LOGGER.info("Acquiring lock with {} threads, {} iterations", threads, iterations); + ExecutorService executorService = Executors.newFixedThreadPool(threads); + final List> futures = executorService.invokeAll(IntStream.range(0, threads) + .mapToObj(tid -> new RepeatedAffinityLocker(cpuIdToLock, iterations)) + .collect(Collectors.toList())); + for (Future future : futures) { + future.get(); + } + executorService.shutdown(); + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + throw new IllegalStateException("Executor service didn't shut down"); + } + } + + @Override + public Void call() throws Exception { + for (int i = 0; i < iterations; i++) { + LOGGER.info("******* Starting iteration {} at {}", i, LocalDateTime.now()); + try (final AffinityLock affinityLock = AffinityLock.acquireLock(cpuIdToLock)) { + if (affinityLock.isAllocated()) { + assertLockFileContainsMyPid(affinityLock); + Thread.sleep(ThreadLocalRandom.current().nextInt(50)); + assertLockFileContainsMyPid(affinityLock); + } else { + LOGGER.info("Couldn't get a lock"); + } + } + } + return null; + } + + private void assertLockFileContainsMyPid(AffinityLock affinityLock) throws IOException { + int lockPID = LockCheck.getProcessForCpu(affinityLock.cpuId()); + if (lockPID != PID) { + throw new IllegalStateException(format("PID in lock file is not mine (lockPID=%d, myPID=%d)", lockPID, PID)); + } + } + } + + /** + * Acquires a lock on the specified CPU, holds it until interrupted + */ + static class AffinityLockerProcess { + + private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLockerProcess.class); + + public static void main(String[] args) { + String cpuIdToLock = args[0]; + + try (final AffinityLock affinityLock = AffinityLock.acquireLock(cpuIdToLock)) { + LOGGER.info("Got affinity lock {} at {}, CPU={}", affinityLock, LocalDateTime.now(), affinityLock.cpuId()); + Thread.sleep(Integer.MAX_VALUE); + LOGGER.error("Woke from sleep? this should never happen"); + } catch (InterruptedException e) { + // expected, just end + LOGGER.info("Interrupted at {} lock is released", LocalDateTime.now()); + } + } + } + + /** + * Acquires a lock then ends + */ + static class AffinityLockerThatDoesNotReleaseProcess { + private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLockerThatDoesNotReleaseProcess.class); + + public static void main(String[] args) { + String cpuIdToLock = args[0]; + + final AffinityLock affinityLock = AffinityLock.acquireLock(cpuIdToLock); + LOGGER.info("Got affinity lock {} at {}, CPU={}", affinityLock, LocalDateTime.now(), affinityLock.cpuId()); + } + } + + /** + * Simulate failing processes by repeatedly dropping lock files + * with PIDs of non-existent processes + */ + static class LockFileDropper { + + public static void main(String[] args) throws InterruptedException, IOException { + while (Thread.currentThread().isInterrupted()) { + for (int cpu = 0; cpu < AffinityLock.PROCESSORS; cpu++) { + try { + File lockFile = toFile(cpu); + try (final FileChannel fc = FileChannel.open(lockFile.toPath(), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { + final long maxValue = Long.MAX_VALUE; // a PID that never exists + ByteBuffer buffer = ByteBuffer.wrap((maxValue + "\n").getBytes(StandardCharsets.UTF_8)); + while (buffer.hasRemaining()) { + //noinspection ResultOfMethodCallIgnored + fc.write(buffer); + } + } + } catch (FileAlreadyExistsException e) { + LOGGER.info("Failed, trying again"); + } + //noinspection BusyWait + Thread.sleep(ThreadLocalRandom.current().nextInt(50)); + } + } + } + + @NotNull + static File toFile(int id) { + return new TestFileLockBasedLockChecker().doToFile(id); + } + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/AbstractAffinityImplTest.java b/affinity/src/test/java/net/openhft/affinity/impl/AbstractAffinityImplTest.java index b21a6a14b..6c1cc5fbf 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/AbstractAffinityImplTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/AbstractAffinityImplTest.java @@ -1,46 +1,31 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; +import net.openhft.affinity.BaseAffinityTest; import net.openhft.affinity.IAffinity; import org.junit.After; -import org.junit.Ignore; import org.junit.Test; import java.util.BitSet; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * @author cheremin * @since 29.12.11, 20:25 */ -public abstract class AbstractAffinityImplTest { +public abstract class AbstractAffinityImplTest extends BaseAffinityTest { - protected static final int CORES = Runtime.getRuntime().availableProcessors(); - protected static final BitSet CORES_MASK = new BitSet(CORES); + private static final int CORES = Runtime.getRuntime().availableProcessors(); + private static final BitSet CORES_MASK = new BitSet(CORES); - static - { + static { CORES_MASK.set(0, CORES, true); } - public abstract IAffinity getImpl(); + protected abstract IAffinity getImpl(); @Test public void getAffinityCompletesGracefully() { @@ -50,15 +35,12 @@ public void getAffinityCompletesGracefully() { @Test public void getAffinityReturnsValidValue() { final BitSet affinity = getImpl().getAffinity(); - assertTrue( - "Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", - !affinity.isEmpty() - ); + assertFalse("Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", affinity.isEmpty()); final long allCoresMask = (1L << CORES) - 1; assertTrue( "Affinity mask " + Utilities.toBinaryString(affinity) + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", - affinity.length() <= CORES_MASK.length() - ); + affinity.length() <= CORES_MASK.length() + ); } @Test @@ -69,11 +51,9 @@ public void setAffinityCompletesGracefully() { } @Test - @Ignore("TODO FIX") public void getAffinityReturnsValuePreviouslySet() { final IAffinity impl = getImpl(); - final int cores = CORES; - for (int core = 0; core < cores; core++) { + for (int core = 0; core < CORES; core++) { final BitSet mask = new BitSet(); mask.set(core, true); getAffinityReturnsValuePreviouslySet(impl, mask); diff --git a/affinity/src/test/java/net/openhft/affinity/impl/CpuInfoLayoutMappingTest.java b/affinity/src/test/java/net/openhft/affinity/impl/CpuInfoLayoutMappingTest.java new file mode 100644 index 000000000..7687f6967 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/CpuInfoLayoutMappingTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.impl; + +import net.openhft.affinity.BaseAffinityTest; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +public class CpuInfoLayoutMappingTest extends BaseAffinityTest { + + @Test + public void verifyI7CpuInfoMapping() throws IOException { + final InputStream i7 = getClass().getClassLoader().getResourceAsStream("i7.cpuinfo"); + VanillaCpuLayout vcl = VanillaCpuLayout.fromCpuInfo(i7); + assertEquals("" + + "0: CpuInfo{socketId=0, coreId=0, threadId=0}\n" + + "1: CpuInfo{socketId=0, coreId=1, threadId=0}\n" + + "2: CpuInfo{socketId=0, coreId=2, threadId=0}\n" + + "3: CpuInfo{socketId=0, coreId=3, threadId=0}\n" + + "4: CpuInfo{socketId=0, coreId=0, threadId=1}\n" + + "5: CpuInfo{socketId=0, coreId=1, threadId=1}\n" + + "6: CpuInfo{socketId=0, coreId=2, threadId=1}\n" + + "7: CpuInfo{socketId=0, coreId=3, threadId=1}\n", + vcl.toString()); + } +} + diff --git a/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityTest.java b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityTest.java new file mode 100644 index 000000000..cf0db12bd --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/LinuxJNAAffinityTest.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.impl; + +import net.openhft.affinity.BaseAffinityTest; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.BitSet; + +import static org.junit.Assert.assertEquals; + +/* + * Created by Peter Lawrey on 23/03/16. + */ +public class LinuxJNAAffinityTest extends BaseAffinityTest { + @BeforeClass + public static void checkJniLibraryPresent() { + Assume.assumeTrue(LinuxJNAAffinity.LOADED); + } + + @Test + public void LinuxJNA() { + int nbits = Runtime.getRuntime().availableProcessors(); + BitSet affinity0 = LinuxJNAAffinity.INSTANCE.getAffinity(); + System.out.println(affinity0); + + BitSet affinity = new BitSet(nbits); + + affinity.set(1); + LinuxJNAAffinity.INSTANCE.setAffinity(affinity); + BitSet affinity2 = LinuxJNAAffinity.INSTANCE.getAffinity(); + System.out.println(affinity2); + assertEquals(1, LinuxJNAAffinity.INSTANCE.getCpu()); + assertEquals(affinity, affinity2); + + affinity.set(0, nbits); + LinuxJNAAffinity.INSTANCE.setAffinity(affinity); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/NativeAffinityImpTest.java b/affinity/src/test/java/net/openhft/affinity/impl/NativeAffinityImpTest.java index 09dafc7d0..2b9cbb73a 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/NativeAffinityImpTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/NativeAffinityImpTest.java @@ -1,6 +1,9 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ package net.openhft.affinity.impl; -import net.openhft.affinity.AffinitySupport; +import net.openhft.affinity.Affinity; import net.openhft.affinity.IAffinity; import org.junit.Assume; import org.junit.BeforeClass; @@ -9,11 +12,10 @@ import static org.junit.Assert.assertTrue; -/** +/* * Created by andre on 22/06/15. */ -public class NativeAffinityImpTest extends AbstractAffinityImplTest -{ +public class NativeAffinityImpTest extends AbstractAffinityImplTest { @BeforeClass public static void checkJniLibraryPresent() { Assume.assumeTrue(NativeAffinity.LOADED); @@ -21,8 +23,7 @@ public static void checkJniLibraryPresent() { } @Override - public IAffinity getImpl() - { + public IAffinity getImpl() { return NativeAffinity.INSTANCE; } @@ -30,7 +31,7 @@ public IAffinity getImpl() public void testGettid() { System.out.println("pid=" + getImpl().getProcessId()); System.out.println("tid=" + getImpl().getThreadId()); - AffinitySupport.setThreadId(); + Affinity.setThreadId(); for (int j = 0; j < 3; j++) { final int runs = 100000; @@ -38,7 +39,9 @@ public void testGettid() { long time = 0; for (int i = 0; i < runs; i++) { long start = System.nanoTime(); - tid = Thread.currentThread().getId(); + @SuppressWarnings("deprecation") + long tid0 = Thread.currentThread().getId(); + tid = tid0; time += System.nanoTime() - start; assertTrue(tid > 0); assertTrue(tid < 1 << 16); diff --git a/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityTest.java b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityTest.java index d2aa694ec..180506e70 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityTest.java @@ -1,29 +1,15 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; import net.openhft.affinity.Affinity; -import net.openhft.affinity.AffinitySupport; import net.openhft.affinity.IAffinity; -import org.junit.Assume; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; /** * @author peter.lawrey @@ -31,7 +17,8 @@ public class PosixJNAAffinityTest extends AbstractAffinityImplTest { @BeforeClass public static void checkJniLibraryPresent() { - Assume.assumeTrue("linux".equalsIgnoreCase(System.getProperty("os.name"))); + assumeTrue("TODO FIX JNA library is not used, but the test is flaky", false); + assumeTrue("linux".equalsIgnoreCase(System.getProperty("os.name"))); } @Override @@ -43,7 +30,7 @@ public IAffinity getImpl() { public void testGettid() { System.out.println("pid=" + getImpl().getProcessId()); System.out.println("tid=" + getImpl().getThreadId()); - AffinitySupport.setThreadId(); + Affinity.setThreadId(); for (int j = 0; j < 3; j++) { final int runs = 100000; @@ -51,10 +38,12 @@ public void testGettid() { long time = 0; for (int i = 0; i < runs; i++) { long start = System.nanoTime(); - tid = Thread.currentThread().getId(); + @SuppressWarnings("deprecation") + long tid0 = Thread.currentThread().getId(); + tid = tid0; time += System.nanoTime() - start; assertTrue(tid > 0); - assertTrue(tid < 1 << 16); + assertTrue(tid < 1 << 24); } System.out.printf("gettid took an average of %,d ns, tid=%d%n", time / runs, tid); } diff --git a/affinity/src/test/java/net/openhft/affinity/impl/UtilitiesTest.java b/affinity/src/test/java/net/openhft/affinity/impl/UtilitiesTest.java new file mode 100644 index 000000000..80eed46ea --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/UtilitiesTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.impl; + +import net.openhft.affinity.BaseAffinityTest; +import org.junit.Test; + +import java.util.BitSet; + +import static org.junit.Assert.assertEquals; + +public class UtilitiesTest extends BaseAffinityTest { + + private static String hex(BitSet set, int... bits) { + set.clear(); + for (int b : bits) { + set.set(b); + } + return Utilities.toHexString(set); + } + + private static String bin(BitSet set, int... bits) { + set.clear(); + for (int b : bits) { + set.set(b); + } + return Utilities.toBinaryString(set); + } + + @Test + public void testToHexString() { + BitSet set = new BitSet(); + assertEquals("", hex(set)); + assertEquals("1", hex(set, 0)); + assertEquals("10", hex(set, 4)); + assertEquals(Long.toHexString(1L << 63), hex(set, 63)); + assertEquals("01", hex(set, 64)); + assertEquals("101", hex(set, 0, 128)); + } + + @Test + public void testToBinaryString() { + BitSet set = new BitSet(); + assertEquals("", bin(set)); + assertEquals("1", bin(set, 0)); + assertEquals("10000", bin(set, 4)); + assertEquals(Long.toBinaryString(1L << 63), bin(set, 63)); + assertEquals("01", bin(set, 64)); + assertEquals("101", bin(set, 0, 128)); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPairTest.java b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPairTest.java new file mode 100644 index 000000000..c356f3c81 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPairTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.impl; + +import net.openhft.affinity.BaseAffinityTest; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +/** + * Tests for {@link VanillaCpuLayout#pair(int)} using sample cpuinfo files. + */ +public class VanillaCpuLayoutPairTest extends BaseAffinityTest { + + @Test + public void testPairForI7() throws IOException { + try (InputStream is = getClass().getClassLoader().getResourceAsStream("i7.cpuinfo")) { + VanillaCpuLayout layout = VanillaCpuLayout.fromCpuInfo(is); + assertEquals(4, layout.pair(0)); + assertEquals(5, layout.pair(1)); + assertEquals(6, layout.pair(2)); + assertEquals(7, layout.pair(3)); + assertEquals(0, layout.pair(4)); + assertEquals(1, layout.pair(5)); + assertEquals(2, layout.pair(6)); + assertEquals(3, layout.pair(7)); + } + } + + @Test + public void testPairForI3() throws IOException { + try (InputStream is = getClass().getClassLoader().getResourceAsStream("i3.cpuinfo")) { + VanillaCpuLayout layout = VanillaCpuLayout.fromCpuInfo(is); + assertEquals(2, layout.pair(0)); + assertEquals(3, layout.pair(1)); + assertEquals(0, layout.pair(2)); + assertEquals(1, layout.pair(3)); + } + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPropertiesParseTest.java b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPropertiesParseTest.java new file mode 100644 index 000000000..7452c060f --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPropertiesParseTest.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.impl; + +import net.openhft.affinity.BaseAffinityTest; +import org.junit.Test; + +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +public class VanillaCpuLayoutPropertiesParseTest extends BaseAffinityTest { + + @Test + public void testCountsI7() throws Exception { + InputStream is = getClass().getClassLoader().getResourceAsStream("i7.properties"); + VanillaCpuLayout vcl = VanillaCpuLayout.fromProperties(is); + assertEquals(8, vcl.cpus()); + assertEquals(1, vcl.sockets()); + assertEquals(4, vcl.coresPerSocket()); + assertEquals(2, vcl.threadsPerCore()); + } + + @Test + public void testCountsDualXeon() throws Exception { + InputStream is = getClass().getClassLoader().getResourceAsStream("dual.xeon.properties"); + VanillaCpuLayout vcl = VanillaCpuLayout.fromProperties(is); + assertEquals(4, vcl.cpus()); + assertEquals(2, vcl.sockets()); + assertEquals(1, vcl.coresPerSocket()); + assertEquals(2, vcl.threadsPerCore()); + } + + @Test + public void testCountsDualE5405() throws Exception { + InputStream is = getClass().getClassLoader().getResourceAsStream("dual.E5405.properties"); + VanillaCpuLayout vcl = VanillaCpuLayout.fromProperties(is); + assertEquals(8, vcl.cpus()); + assertEquals(2, vcl.sockets()); + assertEquals(4, vcl.coresPerSocket()); + assertEquals(1, vcl.threadsPerCore()); + } + + @Test + public void testCountsI3() throws Exception { + InputStream is = getClass().getClassLoader().getResourceAsStream("i3.properties"); + VanillaCpuLayout vcl = VanillaCpuLayout.fromProperties(is); + assertEquals(4, vcl.cpus()); + assertEquals(1, vcl.sockets()); + assertEquals(2, vcl.coresPerSocket()); + assertEquals(2, vcl.threadsPerCore()); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutTest.java b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutTest.java index ae26737ec..0e90d5069 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutTest.java @@ -1,32 +1,20 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.affinity.impl; +import net.openhft.affinity.BaseAffinityTest; import org.junit.Test; import java.io.IOException; import java.io.InputStream; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; /** * @author peter.lawrey */ -public class VanillaCpuLayoutTest { +public class VanillaCpuLayoutTest extends BaseAffinityTest { @Test public void testFromCpuInfoI7() throws IOException { diff --git a/affinity/src/test/java/net/openhft/affinity/impl/VersionHelperTest.java b/affinity/src/test/java/net/openhft/affinity/impl/VersionHelperTest.java new file mode 100644 index 000000000..a7f21afff --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/VersionHelperTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.impl; + +import net.openhft.affinity.BaseAffinityTest; +import org.junit.Assert; +import org.junit.Test; + +public class VersionHelperTest extends BaseAffinityTest { + + @Test + public void isSameOrNewerTest() { + final VersionHelper v0 = new VersionHelper(0, 0, 0); + final VersionHelper v2_6 = new VersionHelper(2, 6, 0); + final VersionHelper v4_1 = new VersionHelper(4, 1, 1); + final VersionHelper v4_9 = new VersionHelper(4, 9, 0); + final VersionHelper v9_9 = new VersionHelper(9, 9, 9); + + VersionHelper[] versions = new VersionHelper[]{v0, v2_6, v4_1, v4_9, v9_9}; + + for (int i = 0; i < versions.length; i++) { + for (int j = 0; j < versions.length; j++) { + Assert.assertEquals(String.format("expected %s.isSameOrNewer(%s) to be %b", versions[i], versions[j], i >= j), + i >= j, versions[i].isSameOrNewer(versions[j])); + } + } + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/testimpl/TestFileLockBasedLockChecker.java b/affinity/src/test/java/net/openhft/affinity/testimpl/TestFileLockBasedLockChecker.java new file mode 100644 index 000000000..56b47d9e1 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/testimpl/TestFileLockBasedLockChecker.java @@ -0,0 +1,15 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.affinity.testimpl; + +import net.openhft.affinity.lockchecker.FileLockBasedLockChecker; + +import java.io.File; + +public class TestFileLockBasedLockChecker extends FileLockBasedLockChecker { + + public File doToFile(int cpu) { + return toFile(cpu); + } +} diff --git a/affinity/src/test/java/net/openhft/ticker/impl/JNIClockTest.java b/affinity/src/test/java/net/openhft/ticker/impl/JNIClockTest.java index 0f2164434..359c06e02 100644 --- a/affinity/src/test/java/net/openhft/ticker/impl/JNIClockTest.java +++ b/affinity/src/test/java/net/openhft/ticker/impl/JNIClockTest.java @@ -1,34 +1,23 @@ /* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package net.openhft.ticker.impl; import net.openhft.affinity.Affinity; +import net.openhft.affinity.BaseAffinityTest; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.assertEquals; -/** - * Created by peter on 13/07/15. +/* + * Created by Peter Lawrey on 13/07/15. */ -public class JNIClockTest { +public class JNIClockTest extends BaseAffinityTest { @Test - public void testNanoTime() throws Exception { + @Ignore("TODO Fix") + public void testNanoTime() throws InterruptedException { for (int i = 0; i < 20000; i++) System.nanoTime(); Affinity.setAffinity(2); @@ -76,4 +65,4 @@ public void testJitter() { System.out.println(((long) (clock.toMicros(time[i]) * 10)) / 10.0 + ", " + ((long) (clock.toMicros(length[i]) * 10) / 10.0)); } } -} \ No newline at end of file +} diff --git a/affinity/src/test/java/org/junit/Assert.java b/affinity/src/test/java/org/junit/Assert.java deleted file mode 100644 index a0325c7bd..000000000 --- a/affinity/src/test/java/org/junit/Assert.java +++ /dev/null @@ -1,885 +0,0 @@ -/* - * Copyright (C) 2015 higherfrequencytrading.com - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package org.junit; - -import org.hamcrest.Matcher; -import org.hamcrest.MatcherAssert; -import org.junit.internal.ArrayComparisonFailure; -import org.junit.internal.ExactComparisonCriteria; -import org.junit.internal.InexactComparisonCriteria; - -/** - * TODO replace with 4.12 when it is released with a simple bug fix. - *

- * A set of assertion methods useful for writing tests. Only failed assertions - * are recorded. These methods can be used directly: - * Assert.assertEquals(...), however, they read better if they - * are referenced through static import:
- *

- *

- * import static org.junit.Assert.*;
- *    ...
- *    assertEquals(...);
- * 
- * - * @see AssertionError - * @since 4.0 - */ -public class Assert { - /** - * Protect constructor since it is a static only class - */ - protected Assert() { - } - - /** - * Asserts that a condition is true. If it isn't it throws an - * {@link AssertionError} with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param condition condition to be checked - */ - static public void assertTrue(String message, boolean condition) { - if (!condition) { - fail(message); - } - } - - /** - * Asserts that a condition is true. If it isn't it throws an - * {@link AssertionError} without a message. - * - * @param condition condition to be checked - */ - static public void assertTrue(boolean condition) { - assertTrue(null, condition); - } - - /** - * Asserts that a condition is false. If it isn't it throws an - * {@link AssertionError} with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param condition condition to be checked - */ - static public void assertFalse(String message, boolean condition) { - assertTrue(message, !condition); - } - - /** - * Asserts that a condition is false. If it isn't it throws an - * {@link AssertionError} without a message. - * - * @param condition condition to be checked - */ - static public void assertFalse(boolean condition) { - assertFalse(null, condition); - } - - /** - * Fails a test with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @see AssertionError - */ - static public void fail(String message) { - if (message == null) { - throw new AssertionError(); - } - throw new AssertionError(message); - } - - /** - * Fails a test with no message. - */ - static public void fail() { - fail(null); - } - - /** - * Asserts that two objects are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. If - * expected and actual are null, - * they are considered equal. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expected expected value - * @param actual actual value - */ - static public void assertEquals(String message, Object expected, - Object actual) { - if (equalsRegardingNull(expected, actual)) { - return; - - } else if (expected instanceof String && actual instanceof String) { - String cleanMessage = message == null ? "" : message; - throw new ComparisonFailure(cleanMessage, (String) expected, - (String) actual); - - } else { - failNotEquals(message, expected, actual); - } - } - - private static boolean equalsRegardingNull(Object expected, Object actual) { - if (expected == null) { - return actual == null; - } - - return isEquals(expected, actual); - } - - private static boolean isEquals(Object expected, Object actual) { - return expected.equals(actual); - } - - /** - * Asserts that two objects are equal. If they are not, an - * {@link AssertionError} without a message is thrown. If - * expected and actual are null, - * they are considered equal. - * - * @param expected expected value - * @param actual the value to check against expected - */ - static public void assertEquals(Object expected, Object actual) { - assertEquals(null, expected, actual); - } - - /** - * Asserts that two objects are not equals. If they are, an - * {@link AssertionError} is thrown with the given message. If - * first and second are null, - * they are considered equal. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param first first value to check - * @param second the value to check against first - */ - static public void assertNotEquals(String message, Object first, - Object second) { - if (equalsRegardingNull(first, second)) { - failEquals(message, first); - } - } - - /** - * Asserts that two objects are not equals. If they are, an - * {@link AssertionError} without a message is thrown. If - * first and second are null, - * they are considered equal. - * - * @param first first value to check - * @param second the value to check against first - */ - static public void assertNotEquals(Object first, Object second) { - assertNotEquals(null, first, second); - } - - private static void failEquals(String message, Object actual) { - String formatted = "Values should be different. "; - if (message != null) { - formatted = message + ". "; - } - - formatted += "Actual: " + actual; - fail(formatted); - } - - /** - * Asserts that two longs are not equals. If they are, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param first first value to check - * @param second the value to check against first - */ - static public void assertNotEquals(String message, long first, long second) { - assertNotEquals(message, (Long) first, (Long) second); - } - - /** - * Asserts that two longs are not equals. If they are, an - * {@link AssertionError} without a message is thrown. - * - * @param first first value to check - * @param second the value to check against first - */ - static public void assertNotEquals(long first, long second) { - assertNotEquals(null, first, second); - } - - /** - * Asserts that two doubles or floats are not equal to within a positive delta. - * If they are, an {@link AssertionError} is thrown with the given - * message. If the expected value is infinity then the delta value is - * ignored. NaNs are considered equal: - * assertNotEquals(Double.NaN, Double.NaN, *) fails - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param first first value to check - * @param second the value to check against first - * @param delta the maximum delta between expected and - * actual for which both numbers are still - * considered equal. - */ - static public void assertNotEquals(String message, double first, - double second, double delta) { - if (!doubleIsDifferent(first, second, delta)) { - failEquals(message, new Double(first)); - } - } - - /** - * Asserts that two doubles or floats are not equal to within a positive delta. - * If they are, an {@link AssertionError} is thrown. If the expected - * value is infinity then the delta value is ignored.NaNs are considered - * equal: assertNotEquals(Double.NaN, Double.NaN, *) fails - * - * @param first first value to check - * @param second the value to check against first - * @param delta the maximum delta between expected and - * actual for which both numbers are still - * considered equal. - */ - static public void assertNotEquals(double first, double second, double delta) { - assertNotEquals(null, first, second, delta); - } - - /** - * Asserts that two object arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. If - * expecteds and actuals are null, - * they are considered equal. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds Object array or array of arrays (multi-dimensional array) with - * expected values. - * @param actuals Object array or array of arrays (multi-dimensional array) with - * actual values - */ - public static void assertArrayEquals(String message, Object[] expecteds, - Object[] actuals) throws ArrayComparisonFailure { - internalArrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two object arrays are equal. If they are not, an - * {@link AssertionError} is thrown. If expected and - * actual are null, they are considered - * equal. - * - * @param expecteds Object array or array of arrays (multi-dimensional array) with - * expected values - * @param actuals Object array or array of arrays (multi-dimensional array) with - * actual values - */ - public static void assertArrayEquals(Object[] expecteds, Object[] actuals) { - assertArrayEquals(null, expecteds, actuals); - } - - /** - * Asserts that two byte arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds byte array with expected values. - * @param actuals byte array with actual values - */ - public static void assertArrayEquals(String message, byte[] expecteds, - byte[] actuals) throws ArrayComparisonFailure { - internalArrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two byte arrays are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expecteds byte array with expected values. - * @param actuals byte array with actual values - */ - public static void assertArrayEquals(byte[] expecteds, byte[] actuals) { - assertArrayEquals(null, expecteds, actuals); - } - - /** - * Asserts that two char arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds char array with expected values. - * @param actuals char array with actual values - */ - public static void assertArrayEquals(String message, char[] expecteds, - char[] actuals) throws ArrayComparisonFailure { - internalArrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two char arrays are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expecteds char array with expected values. - * @param actuals char array with actual values - */ - public static void assertArrayEquals(char[] expecteds, char[] actuals) { - assertArrayEquals(null, expecteds, actuals); - } - - /** - * Asserts that two short arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds short array with expected values. - * @param actuals short array with actual values - */ - public static void assertArrayEquals(String message, short[] expecteds, - short[] actuals) throws ArrayComparisonFailure { - internalArrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two short arrays are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expecteds short array with expected values. - * @param actuals short array with actual values - */ - public static void assertArrayEquals(short[] expecteds, short[] actuals) { - assertArrayEquals(null, expecteds, actuals); - } - - /** - * Asserts that two int arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds int array with expected values. - * @param actuals int array with actual values - */ - public static void assertArrayEquals(String message, int[] expecteds, - int[] actuals) throws ArrayComparisonFailure { - internalArrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two int arrays are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expecteds int array with expected values. - * @param actuals int array with actual values - */ - public static void assertArrayEquals(int[] expecteds, int[] actuals) { - assertArrayEquals(null, expecteds, actuals); - } - - /** - * Asserts that two long arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds long array with expected values. - * @param actuals long array with actual values - */ - public static void assertArrayEquals(String message, long[] expecteds, - long[] actuals) throws ArrayComparisonFailure { - internalArrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two long arrays are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expecteds long array with expected values. - * @param actuals long array with actual values - */ - public static void assertArrayEquals(long[] expecteds, long[] actuals) { - assertArrayEquals(null, expecteds, actuals); - } - - /** - * Asserts that two double arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds double array with expected values. - * @param actuals double array with actual values - */ - public static void assertArrayEquals(String message, double[] expecteds, - double[] actuals, double delta) throws ArrayComparisonFailure { - new InexactComparisonCriteria(delta).arrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two double arrays are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expecteds double array with expected values. - * @param actuals double array with actual values - */ - public static void assertArrayEquals(double[] expecteds, double[] actuals, double delta) { - assertArrayEquals(null, expecteds, actuals, delta); - } - - /** - * Asserts that two float arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds float array with expected values. - * @param actuals float array with actual values - */ - public static void assertArrayEquals(String message, float[] expecteds, - float[] actuals, float delta) throws ArrayComparisonFailure { - new InexactComparisonCriteria(delta).arrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two float arrays are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expecteds float array with expected values. - * @param actuals float array with actual values - */ - public static void assertArrayEquals(float[] expecteds, float[] actuals, float delta) { - assertArrayEquals(null, expecteds, actuals, delta); - } - - /** - * Asserts that two object arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. If - * expecteds and actuals are null, - * they are considered equal. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds Object array or array of arrays (multi-dimensional array) with - * expected values. - * @param actuals Object array or array of arrays (multi-dimensional array) with - * actual values - */ - private static void internalArrayEquals(String message, Object expecteds, - Object actuals) throws ArrayComparisonFailure { - new ExactComparisonCriteria().arrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two doubles are equal to within a positive delta. - * If they are not, an {@link AssertionError} is thrown with the given - * message. If the expected value is infinity then the delta value is - * ignored. NaNs are considered equal: - * assertEquals(Double.NaN, Double.NaN, *) passes - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expected expected value - * @param actual the value to check against expected - * @param delta the maximum delta between expected and - * actual for which both numbers are still - * considered equal. - */ - static public void assertEquals(String message, double expected, - double actual, double delta) { - if (doubleIsDifferent(expected, actual, delta)) { - failNotEquals(message, new Double(expected), new Double(actual)); - } - } - - /** - * Asserts that two floats are equal to within a positive delta. - * If they are not, an {@link AssertionError} is thrown with the given - * message. If the expected value is infinity then the delta value is - * ignored. NaNs are considered equal: - * assertEquals(Float.NaN, Float.NaN, *) passes - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expected expected value - * @param actual the value to check against expected - * @param delta the maximum delta between expected and - * actual for which both numbers are still - * considered equal. - */ - static public void assertEquals(String message, float expected, - float actual, float delta) { - if (Float.compare(expected, actual) == 0) { - return; - } - if (!(Math.abs(expected - actual) <= delta)) { - failNotEquals(message, new Float(expected), new Float(actual)); - } - } - - static private boolean doubleIsDifferent(double d1, double d2, double delta) { - if (Double.compare(d1, d2) == 0) { - return false; - } - return (Math.abs(d1 - d2) > delta); - - } - - /** - * Asserts that two longs are equal. If they are not, an - * {@link AssertionError} is thrown. - * - * @param expected expected long value. - * @param actual actual long value - */ - static public void assertEquals(long expected, long actual) { - if (expected != actual) - assertEquals(null, expected, actual); - } - - /** - * Asserts that two longs are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expected long expected value. - * @param actual long actual value - */ - static public void assertEquals(String message, long expected, long actual) { - assertEquals(message, (Long) expected, (Long) actual); - } - - /** - * @deprecated Use - * assertEquals(double expected, double actual, double delta) - * instead - */ - @Deprecated - static public void assertEquals(double expected, double actual) { - assertEquals(null, expected, actual); - } - - /** - * @deprecated Use - * assertEquals(String message, double expected, double actual, double delta) - * instead - */ - @Deprecated - static public void assertEquals(String message, double expected, - double actual) { - fail("Use assertEquals(expected, actual, delta) to compare floating-point numbers"); - } - - /** - * Asserts that two doubles are equal to within a positive delta. - * If they are not, an {@link AssertionError} is thrown. If the expected - * value is infinity then the delta value is ignored.NaNs are considered - * equal: assertEquals(Double.NaN, Double.NaN, *) passes - * - * @param expected expected value - * @param actual the value to check against expected - * @param delta the maximum delta between expected and - * actual for which both numbers are still - * considered equal. - */ - static public void assertEquals(double expected, double actual, double delta) { - assertEquals(null, expected, actual, delta); - } - - /** - * Asserts that two floats are equal to within a positive delta. - * If they are not, an {@link AssertionError} is thrown. If the expected - * value is infinity then the delta value is ignored. NaNs are considered - * equal: assertEquals(Float.NaN, Float.NaN, *) passes - * - * @param expected expected value - * @param actual the value to check against expected - * @param delta the maximum delta between expected and - * actual for which both numbers are still - * considered equal. - */ - static public void assertEquals(float expected, float actual, float delta) { - assertEquals(null, expected, actual, delta); - } - - /** - * Asserts that an object isn't null. If it is an {@link AssertionError} is - * thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param object Object to check or null - */ - static public void assertNotNull(String message, Object object) { - assertTrue(message, object != null); - } - - /** - * Asserts that an object isn't null. If it is an {@link AssertionError} is - * thrown. - * - * @param object Object to check or null - */ - static public void assertNotNull(Object object) { - assertNotNull(null, object); - } - - /** - * Asserts that an object is null. If it is not, an {@link AssertionError} - * is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param object Object to check or null - */ - static public void assertNull(String message, Object object) { - if (object == null) { - return; - } - failNotNull(message, object); - } - - /** - * Asserts that an object is null. If it isn't an {@link AssertionError} is - * thrown. - * - * @param object Object to check or null - */ - static public void assertNull(Object object) { - assertNull(null, object); - } - - static private void failNotNull(String message, Object actual) { - String formatted = ""; - if (message != null) { - formatted = message + " "; - } - fail(formatted + "expected null, but was:<" + actual + ">"); - } - - /** - * Asserts that two objects refer to the same object. If they are not, an - * {@link AssertionError} is thrown with the given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expected the expected object - * @param actual the object to compare to expected - */ - static public void assertSame(String message, Object expected, Object actual) { - if (expected == actual) { - return; - } - failNotSame(message, expected, actual); - } - - /** - * Asserts that two objects refer to the same object. If they are not the - * same, an {@link AssertionError} without a message is thrown. - * - * @param expected the expected object - * @param actual the object to compare to expected - */ - static public void assertSame(Object expected, Object actual) { - assertSame(null, expected, actual); - } - - /** - * Asserts that two objects do not refer to the same object. If they do - * refer to the same object, an {@link AssertionError} is thrown with the - * given message. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param unexpected the object you don't expect - * @param actual the object to compare to unexpected - */ - static public void assertNotSame(String message, Object unexpected, - Object actual) { - if (unexpected == actual) { - failSame(message); - } - } - - /** - * Asserts that two objects do not refer to the same object. If they do - * refer to the same object, an {@link AssertionError} without a message is - * thrown. - * - * @param unexpected the object you don't expect - * @param actual the object to compare to unexpected - */ - static public void assertNotSame(Object unexpected, Object actual) { - assertNotSame(null, unexpected, actual); - } - - static private void failSame(String message) { - String formatted = ""; - if (message != null) { - formatted = message + " "; - } - fail(formatted + "expected not same"); - } - - static private void failNotSame(String message, Object expected, - Object actual) { - String formatted = ""; - if (message != null) { - formatted = message + " "; - } - fail(formatted + "expected same:<" + expected + "> was not:<" + actual - + ">"); - } - - static private void failNotEquals(String message, Object expected, - Object actual) { - fail(format(message, expected, actual)); - } - - static String format(String message, Object expected, Object actual) { - String formatted = ""; - if (message != null && !message.equals("")) { - formatted = message + " "; - } - String expectedString = String.valueOf(expected); - String actualString = String.valueOf(actual); - if (expectedString.equals(actualString)) { - return formatted + "expected: " - + formatClassAndValue(expected, expectedString) - + " but was: " + formatClassAndValue(actual, actualString); - - } else { - return formatted + "expected:<" + expectedString + "> but was:<" - + actualString + ">"; - } - } - - private static String formatClassAndValue(Object value, String valueString) { - String className = value == null ? "null" : value.getClass().getName(); - return className + "<" + valueString + ">"; - } - - /** - * Asserts that two object arrays are equal. If they are not, an - * {@link AssertionError} is thrown with the given message. If - * expecteds and actuals are null, - * they are considered equal. - * - * @param message the identifying message for the {@link AssertionError} (null - * okay) - * @param expecteds Object array or array of arrays (multi-dimensional array) with - * expected values. - * @param actuals Object array or array of arrays (multi-dimensional array) with - * actual values - * @deprecated use assertArrayEquals - */ - @Deprecated - public static void assertEquals(String message, Object[] expecteds, - Object[] actuals) { - assertArrayEquals(message, expecteds, actuals); - } - - /** - * Asserts that two object arrays are equal. If they are not, an - * {@link AssertionError} is thrown. If expected and - * actual are null, they are considered - * equal. - * - * @param expecteds Object array or array of arrays (multi-dimensional array) with - * expected values - * @param actuals Object array or array of arrays (multi-dimensional array) with - * actual values - * @deprecated use assertArrayEquals - */ - @Deprecated - public static void assertEquals(Object[] expecteds, Object[] actuals) { - assertArrayEquals(expecteds, actuals); - } - - /** - * Asserts that actual satisfies the condition specified by - * matcher. If not, an {@link AssertionError} is thrown with - * information about the matcher and failing value. Example: - *

- *

-     *   assertThat(0, is(1)); // fails:
-     *     // failure message:
-     *     // expected: is <1>
-     *     // got value: <0>
-     *   assertThat(0, is(not(1))) // passes
-     * 
- *

- * org.hamcrest.Matcher does not currently document the meaning - * of its type parameter T. This method assumes that a matcher - * typed as Matcher<T> can be meaningfully applied only - * to values that could be assigned to a variable of type T. - * - * @param the static type accepted by the matcher (this can flag obvious - * compile-time problems such as {@code assertThat(1, is("a"))} - * @param actual the computed value being compared - * @param matcher an expression, built of {@link Matcher}s, specifying allowed - * values - * @see org.hamcrest.CoreMatchers - * @see org.hamcrest.MatcherAssert - */ - public static void assertThat(T actual, Matcher matcher) { - assertThat("", actual, matcher); - } - - /** - * Asserts that actual satisfies the condition specified by - * matcher. If not, an {@link AssertionError} is thrown with - * the reason and information about the matcher and failing value. Example: - *

- *

-     *   assertThat("Help! Integers don't work", 0, is(1)); // fails:
-     *     // failure message:
-     *     // Help! Integers don't work
-     *     // expected: is <1>
-     *     // got value: <0>
-     *   assertThat("Zero is one", 0, is(not(1))) // passes
-     * 
- *

- * org.hamcrest.Matcher does not currently document the meaning - * of its type parameter T. This method assumes that a matcher - * typed as Matcher<T> can be meaningfully applied only - * to values that could be assigned to a variable of type T. - * - * @param reason additional information about the error - * @param the static type accepted by the matcher (this can flag obvious - * compile-time problems such as {@code assertThat(1, is("a"))} - * @param actual the computed value being compared - * @param matcher an expression, built of {@link Matcher}s, specifying allowed - * values - * @see org.hamcrest.CoreMatchers - * @see org.hamcrest.MatcherAssert - */ - public static void assertThat(String reason, T actual, - Matcher matcher) { - MatcherAssert.assertThat(reason, actual, matcher); - } -} diff --git a/affinity/src/test/java/software/chronicle/enterprise/internals/JnaAffinityTest.java b/affinity/src/test/java/software/chronicle/enterprise/internals/JnaAffinityTest.java new file mode 100644 index 000000000..a65863c1d --- /dev/null +++ b/affinity/src/test/java/software/chronicle/enterprise/internals/JnaAffinityTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package software.chronicle.enterprise.internals; + +import net.openhft.affinity.BaseAffinityTest; +import net.openhft.affinity.IAffinity; +import net.openhft.affinity.impl.LinuxJNAAffinity; +import net.openhft.affinity.impl.Utilities; +import org.junit.After; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.BitSet; + +import static org.junit.Assert.*; + +/** + * @author peter.lawrey + */ +public class JnaAffinityTest extends BaseAffinityTest { + private static final int CORES = Runtime.getRuntime().availableProcessors(); + private static final BitSet CORES_MASK = new BitSet(CORES); + + static { + CORES_MASK.set(0, CORES, true); + } + + @BeforeClass + public static void checkJniLibraryPresent() { + Assume.assumeTrue(LinuxJNAAffinity.LOADED); + } + + @Test + public void getAffinityCompletesGracefully() { + System.out.println("affinity: " + Utilities.toBinaryString(getImpl().getAffinity())); + } + + @Test + public void getAffinityReturnsValidValue() { + final BitSet affinity = getImpl().getAffinity(); + assertFalse("Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", affinity.isEmpty()); + final int allCoresMask = (1 << CORES) - 1; + assertTrue( + "Affinity mask " + Utilities.toBinaryString(affinity) + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", + affinity.length() <= CORES_MASK.length() + ); + } + + @Test + public void setAffinityCompletesGracefully() { + BitSet affinity = new BitSet(1); + affinity.set(0, true); + getImpl().setAffinity(affinity); + } + + @Test + public void getAffinityReturnsValuePreviouslySet() { + String osName = System.getProperty("os.name"); + if (!osName.startsWith("Linux")) { + System.out.println("Skipping Linux tests"); + return; + } + final IAffinity impl = LinuxJNAAffinity.INSTANCE; + for (int core = 0; core < CORES; core++) { + final BitSet mask = new BitSet(); + mask.set(core, true); + getAffinityReturnsValuePreviouslySet(impl, mask); + } + } + + @Test + public void showOtherIds() { + System.out.println("processId: " + LinuxJNAAffinity.INSTANCE.getProcessId()); + System.out.println("threadId: " + LinuxJNAAffinity.INSTANCE.getThreadId()); + System.out.println("cpu: " + LinuxJNAAffinity.INSTANCE.getCpu()); + } + + private void getAffinityReturnsValuePreviouslySet(final IAffinity impl, + final BitSet mask) { + + impl.setAffinity(mask); + final BitSet _mask = impl.getAffinity(); + assertEquals(mask, _mask); + } + + @After + public void tearDown() { + getImpl().setAffinity(CORES_MASK); + } + + private IAffinity getImpl() { + return LinuxJNAAffinity.INSTANCE; + } +} diff --git a/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java b/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java index b0a77af98..fa2bc7fba 100644 --- a/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java +++ b/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java @@ -1,21 +1,9 @@ /* - * Copyright 2011 Peter Lawrey - * - * 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. + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 */ - package software.chronicle.enterprise.internals; +import net.openhft.affinity.BaseAffinityTest; import net.openhft.affinity.IAffinity; import net.openhft.affinity.impl.LinuxJNAAffinity; import net.openhft.affinity.impl.Utilities; @@ -24,18 +12,16 @@ import java.util.BitSet; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * @author peter.lawrey */ -public class NativeAffinityTest { - protected static final int CORES = Runtime.getRuntime().availableProcessors(); - protected static final BitSet CORES_MASK = new BitSet(CORES); +public class NativeAffinityTest extends BaseAffinityTest { + private static final int CORES = Runtime.getRuntime().availableProcessors(); + private static final BitSet CORES_MASK = new BitSet(CORES); - static - { + static { CORES_MASK.set(0, CORES, true); } @@ -52,15 +38,12 @@ public void getAffinityCompletesGracefully() { @Test public void getAffinityReturnsValidValue() { final BitSet affinity = getImpl().getAffinity(); - assertTrue( - "Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", - !affinity.isEmpty() - ); + assertFalse("Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", affinity.isEmpty()); final int allCoresMask = (1 << CORES) - 1; assertTrue( "Affinity mask " + Utilities.toBinaryString(affinity) + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", - affinity.length() <= CORES_MASK.length() - ); + affinity.length() <= CORES_MASK.length() + ); } @Test @@ -79,8 +62,7 @@ public void getAffinityReturnsValuePreviouslySet() { return; } final IAffinity impl = NativeAffinity.INSTANCE; - final int cores = CORES; - for (int core = 0; core < cores; core++) { + for (int core = 0; core < CORES; core++) { final BitSet mask = new BitSet(); mask.set(core, true); getAffinityReturnsValuePreviouslySet(impl, mask); @@ -134,7 +116,7 @@ public void tearDown() { getImpl().setAffinity(CORES_MASK); } - public IAffinity getImpl() { + private IAffinity getImpl() { return NativeAffinity.INSTANCE; } } diff --git a/affinity/src/test/resources/dual.E5405.properties b/affinity/src/test/resources/dual.E5405.properties new file mode 100644 index 000000000..d656b7376 --- /dev/null +++ b/affinity/src/test/resources/dual.E5405.properties @@ -0,0 +1,24 @@ +# +# 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. +# +# +0=0,0,0 +1=0,1,0 +2=0,2,0 +3=0,3,0 +4=1,4,0 +5=1,5,0 +6=1,6,0 +7=1,7,0 diff --git a/affinity/src/test/resources/dual.xeon.properties b/affinity/src/test/resources/dual.xeon.properties new file mode 100644 index 000000000..36fc4ef90 --- /dev/null +++ b/affinity/src/test/resources/dual.xeon.properties @@ -0,0 +1,20 @@ +# +# 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. +# +# +0=0,0,0 +1=0,0,1 +2=3,3,0 +3=3,3,1 diff --git a/affinity/src/test/resources/i3.properties b/affinity/src/test/resources/i3.properties new file mode 100644 index 000000000..ab4e83876 --- /dev/null +++ b/affinity/src/test/resources/i3.properties @@ -0,0 +1,20 @@ +# +# 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. +# +# +0=0,0,0 +1=0,2,0 +2=0,0,1 +3=0,2,1 diff --git a/affinity/src/test/resources/i7.properties b/affinity/src/test/resources/i7.properties index 39117a318..9e617cf9c 100644 --- a/affinity/src/test/resources/i7.properties +++ b/affinity/src/test/resources/i7.properties @@ -1,19 +1,19 @@ # -# Copyright (C) 2015 higherfrequencytrading.com +# Copyright 2016-2025 chronicle.software # -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of 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 # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. +# 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. # -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . # - 0=0,0,0 1=0,1,0 2=0,2,0 diff --git a/docs/images/Thread-Affinity_line.png b/docs/images/Thread-Affinity_line.png new file mode 100644 index 000000000..2f80359d2 Binary files /dev/null and b/docs/images/Thread-Affinity_line.png differ diff --git a/pom.xml b/pom.xml index bbac88059..f88f45845 100644 --- a/pom.xml +++ b/pom.xml @@ -1,37 +1,25 @@ - - + + Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + +--> + + + 4.0.0 net.openhft - root-parent-pom - 1.1.1 - + java-parent-pom + 2026.0 + - 4.0.0 Java-Thread-Affinity - 3.0-SNAPSHOT + 2026.3-SNAPSHOT pom - Java Affinity Parent + OpenHFT/Java-Thread-Affinity Parent Java Thread Affinity library @@ -39,4 +27,12 @@ affinity-test + + scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git + scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git + scm:git:git@github.com:OpenHFT/Java-Thread-Affinity.git + + ea + +