diff --git a/.gitignore b/.gitignore index 3a98cfba8..00caab13d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,53 @@ +### How to update +# This is copied from OpenHFT/.gitignore +# update the original and run OpenHFT/update_gitignore.sh + +### Compiled class file *.class -*.iml -# Package Files # +### Package Files *.jar *.war *.ear + +### 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 +hs_err_pid* +### Maven template target/ -.idea +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 +### Queue files +*.cq4t +*.cq4 diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 37ec93a14..000000000 --- a/LICENSE +++ /dev/null @@ -1,191 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright -owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities -that control, are controlled by, or are under common control with that entity. -For the purposes of this definition, "control" means (i) the power, direct or -indirect, to cause the direction or management of such entity, whether by -contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the -outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including -but not limited to software source code, documentation source, and configuration -files. - -"Object" form shall mean any form resulting from mechanical transformation or -translation of a Source form, including but not limited to compiled object code, -generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made -available under the License, as indicated by a copyright notice that is included -in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that -is based on (or derived from) the Work and for which the editorial revisions, -annotations, elaborations, or other modifications represent, as a whole, an -original work of authorship. For the purposes of this License, Derivative Works -shall not include works that remain separable from, or merely link (or bind by -name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version -of the Work and any modifications or additions to that Work or Derivative Works -thereof, that is intentionally submitted to Licensor for inclusion in the Work -by the copyright owner or by an individual or Legal Entity authorized to submit -on behalf of the copyright owner. For the purposes of this definition, -"submitted" means any form of electronic, verbal, or written communication sent -to the Licensor or its representatives, including but not limited to -communication on electronic mailing lists, source code control systems, and -issue tracking systems that are managed by, or on behalf of, the Licensor for -the purpose of discussing and improving the Work, but excluding communication -that is conspicuously marked or otherwise designated in writing by the copyright -owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf -of whom a Contribution has been received by Licensor and subsequently -incorporated within the Work. - -2. Grant of Copyright License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable copyright license to reproduce, prepare Derivative Works of, -publicly display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form. - -3. Grant of Patent License. - -Subject to the terms and conditions of this License, each Contributor hereby -grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, -irrevocable (except as stated in this section) patent license to make, have -made, use, offer to sell, sell, import, and otherwise transfer the Work, where -such license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by combination -of their Contribution(s) with the Work to which such Contribution(s) was -submitted. If You institute patent litigation against any entity (including a -cross-claim or counterclaim in a lawsuit) alleging that the Work or a -Contribution incorporated within the Work constitutes direct or contributory -patent infringement, then any patent licenses granted to You under this License -for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. - -You may reproduce and distribute copies of the Work or Derivative Works thereof -in any medium, with or without modifications, and in Source or Object form, -provided that You meet the following conditions: - -You must give any other recipients of the Work or Derivative Works a copy of -this License; and -You must cause any modified files to carry prominent notices stating that You -changed the files; and -You must retain, in the Source form of any Derivative Works that You distribute, -all copyright, patent, trademark, and attribution notices from the Source form -of the Work, excluding those notices that do not pertain to any part of the -Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any -Derivative Works that You distribute must include a readable copy of the -attribution notices contained within such NOTICE file, excluding those notices -that do not pertain to any part of the Derivative Works, in at least one of the -following places: within a NOTICE text file distributed as part of the -Derivative Works; within the Source form or documentation, if provided along -with the Derivative Works; or, within a display generated by the Derivative -Works, if and wherever such third-party notices normally appear. The contents of -the NOTICE file are for informational purposes only and do not modify the -License. You may add Your own attribution notices within Derivative Works that -You distribute, alongside or as an addendum to the NOTICE text from the Work, -provided that such additional attribution notices cannot be construed as -modifying the License. -You may add Your own copyright statement to Your modifications and may provide -additional or different license terms and conditions for use, reproduction, or -distribution of Your modifications, or for any such Derivative Works as a whole, -provided Your use, reproduction, and distribution of the Work otherwise complies -with the conditions stated in this License. - -5. Submission of Contributions. - -Unless You explicitly state otherwise, any Contribution intentionally submitted -for inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the terms of -any separate license agreement you may have executed with Licensor regarding -such Contributions. - -6. Trademarks. - -This License does not grant permission to use the trade names, trademarks, -service marks, or product names of the Licensor, except as required for -reasonable and customary use in describing the origin of the Work and -reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. - -Unless required by applicable law or agreed to in writing, Licensor provides the -Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, -including, without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are -solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise of -permissions under this License. - -8. Limitation of Liability. - -In no event and under no legal theory, whether in tort (including negligence), -contract, or otherwise, unless required by applicable law (such as deliberate -and grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, incidental, -or consequential damages of any character arising as a result of this License or -out of the use or inability to use the Work (including but not limited to -damages for loss of goodwill, work stoppage, computer failure or malfunction, or -any and all other commercial damages or losses), even if such Contributor has -been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. - -While redistributing the Work or Derivative Works thereof, You may choose to -offer, and charge a fee for, acceptance of support, warranty, indemnity, or -other liability obligations and/or rights consistent with this License. However, -in accepting such obligations, You may act only on Your own behalf and on Your -sole responsibility, not on behalf of any other Contributor, and only if You -agree to indemnify, defend, and hold each Contributor harmless for any liability -incurred by, or claims asserted against, such Contributor by reason of your -accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work - -To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included on -the same "printed page" as the copyright notice for easier identification within -third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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/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 05e5cc3d8..000000000 --- a/README.md +++ /dev/null @@ -1,18 +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.0.1 - Added getThreadId for the process if of the thread. - -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 diff --git a/affinity-test/pom.xml b/affinity-test/pom.xml index 5ea3bbc88..3ce950e34 100644 --- a/affinity-test/pom.xml +++ b/affinity-test/pom.xml @@ -1,49 +1,40 @@ - - + + Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + +--> + + + 4.0.0 net.openhft java-parent-pom - 1.1.2 - + 2026.0 + - 4.0.0 - affinity-test - 2.1.7-SNAPSHOT + 2026.3-SNAPSHOT bundle OpenHFT/Java-Thread-Affinity/affinity-test Java Thread Affinity library - + + + UTF-8 + 0 + 0 + + net.openhft third-party-bom + 2026.0 pom - 3.4.11 import @@ -55,17 +46,13 @@ - - com.sun.java - tools - net.openhft affinity - javax.inject - javax.inject + jakarta.inject + jakarta.inject-api test @@ -107,7 +94,6 @@ org.apache.felix org.apache.felix.framework - 4.4.1 test @@ -124,6 +110,16 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + -Xlint:deprecation + 1.8 + 1.8 + UTF-8 + + + + Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + +--> 4.0.0 @@ -23,19 +11,22 @@ net.openhft java-parent-pom - 1.1.2 + 2026.0 affinity - 2.1.7-SNAPSHOT + 2026.3-SNAPSHOT bundle OpenHFT/Java-Thread-Affinity/affinity Java Thread Affinity library + src/main/c UTF-8 + 0.40 + 0.4 @@ -43,16 +34,21 @@ net.openhft third-party-bom + 2026.0 + pom + import + + + net.openhft + chronicle-bom + 2026.0-SNAPSHOT pom - 3.4.11 import - - org.slf4j slf4j-api @@ -66,7 +62,7 @@ jna-platform - org.kohsuke.jetbrains + org.jetbrains annotations @@ -85,11 +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 + + + -h + ${project.build.directory}/jni + -Xlint:all,-options + + 1.8 + 1.8 + UTF-8 + + + org.apache.maven.plugins maven-scm-publish-plugin @@ -103,22 +166,42 @@ gh-pages + + + org.apache.servicemix.tooling + depends-maven-plugin + + + generate-depends-file + + generate-depends-file + + + + org.apache.felix maven-bundle-plugin true - ${project.groupId}.${project.artifactId} - - ${project.artifactId} + ${project.groupId}.${project.artifactId} + OpenHFT :: ${project.artifactId} + ${project.version} - com.sun.jna.platform.win32;resolution:=optional, com.sun.jna.platform.*;resolution:=optional, * - net.openhft.affinity.* + net.openhft.affinity.*;-noimport:=true, + net.openhft.ticker.*;-noimport:=true @@ -137,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 @@ -153,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 new file mode 100755 index 000000000..ac448e568 --- /dev/null +++ b/affinity/src/main/c/Makefile @@ -0,0 +1,51 @@ +# +# Makefile for C code +# + +# C sources to compile + +TARGET_DIR := ../../../target/classes +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_SOURCES := $(patsubst %,%.cpp,$(JNI_STUBS)) + +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" -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/$(JNI_OS)" -I"$(WORKING_DIR)" + +# classpath for javah +ifdef CLASSPATH +JAVAH_CLASSPATH = $(JAVA_BUILD_DIR):$(CLASSPATH) +else +JAVAH_CLASSPATH = $(JAVA_BUILD_DIR) +endif + +.PHONY : clean + +all: $(TARGET) + +$(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 new file mode 100644 index 000000000..2cf233d3a --- /dev/null +++ b/affinity/src/main/c/net_openhft_ticker_impl_JNIClock.cpp @@ -0,0 +1,77 @@ +/** + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include "net_openhft_ticker_impl_JNIClock.h" + +#if defined(__i386__) +static __inline__ unsigned long long rdtsc(void) { + unsigned long long int x; + __asm__ volatile (".byte 0x0f, 0x31" : "=A" (x)); + return x; +} + +#elif defined(__x86_64__) +static __inline__ unsigned long long rdtsc(void) { + unsigned hi, lo; + __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); + return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 ); +} + +#elif defined(__MIPS_32__) +#define rdtsc(dest) \ + _ _asm_ _ _ _volatile_ _("mfc0 %0,$9; nop" : "=r" (dest)) + +#elif defined(__MIPS_SGI__) +#include + +static __inline__ unsigned long long rdtsc (void) { + struct timespec tp; + 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 + +/* + * Class: net_openhft_clock_impl_JNIClock + * Method: rdtsc0 + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_net_openhft_ticker_impl_JNIClock_rdtsc0 + (JNIEnv *env, jclass c) { + return (jlong) rdtsc(); +} 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 new file mode 100644 index 000000000..f13d566a1 --- /dev/null +++ b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity.cpp @@ -0,0 +1,117 @@ +/** + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#ifdef __linux__ + #include + #include + #include + #include + #include +#endif +#include +#include "software_chronicle_enterprise_internals_impl_NativeAffinity.h" + +/* + * Class: software_chronicle_enterprise_internals_impl_NativeAffinity + * Method: getAffinity0 + * Signature: ()J + */ +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 + cpu_set_t mask; + const size_t size = sizeof(mask); + + int res = sched_getaffinity(0, size, &mask); + if (res < 0) + { + return NULL; + } + + jbyteArray ret = env->NewByteArray(size); + jbyte* bytes = env->GetByteArrayElements(ret, 0); + memcpy(bytes, &mask, size); + env->SetByteArrayRegion(ret, 0, size, bytes); + + return ret; +#else + throw std::runtime_error("Not supported"); +#endif +} + +/* + * Class: software_chronicle_enterprise_internals_NativeAffinity + * Method: setAffinity0 + * Signature: (J)V + */ +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); + + jbyte* bytes = env->GetByteArrayElements(affinity, 0); + memcpy(&mask, bytes, size); + + sched_setaffinity(0, size, &mask); +#else + throw std::runtime_error("Not supported"); +#endif +} + +/* + * Class: software_chronicle_enterprise_internals_impl_NativeAffinity + * Method: getProcessId0 + * Signature: ()I + */ +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 +} + +/* + * Class: software_chronicle_enterprise_internals_impl_NativeAffinity + * Method: getThreadId0 + * Signature: ()I + */ +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 +} + +/* + * Class: software_chronicle_enterprise_internals_impl_NativeAffinity + * Method: getCpu0 + * Signature: ()I + */ +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 new file mode 100644 index 000000000..67dafb1ef --- /dev/null +++ b/affinity/src/main/c/software_chronicle_enterprise_internals_impl_NativeAffinity_MacOSX.c @@ -0,0 +1,55 @@ +/** + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "software_chronicle_enterprise_internals_impl_NativeAffinity.h" + +/* + * Class: software_chronicle_enterprise_internals_impl_NativeAffinity + * Method: getAffinity0 + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_getAffinity0 + (JNIEnv *env, jclass c) { + + thread_port_t threadport = pthread_mach_thread_np(pthread_self()); + + struct thread_enterprise_internals_policy policy; + policy.affinity_tag = 0; + mach_msg_type_number_t count = THREAD_AFFINITY_POLICY_COUNT; + boolean_t get_default = FALSE; + + if ((thread_policy_get(threadport, + THREAD_AFFINITY_POLICY, (thread_policy_t)&policy, + &count, &get_default)) != KERN_SUCCESS) { + return ~0LL; + } + + return (jlong) policy.affinity_tag; +} + +/* + * Class: software_chronicle_enterprise_internals_NativeAffinity + * Method: setAffinity0 + * Signature: (J)V + */ +JNIEXPORT void JNICALL Java_software_chronicle_enterprise_internals_impl_NativeAffinity_setAffinity0 + (JNIEnv *env, jclass c, jlong affinity) { + + thread_port_t threadport = pthread_mach_thread_np(pthread_self()); + + struct thread_enterprise_internals_policy policy; + policy.affinity_tag = affinity; + + int rc = thread_policy_set(threadport, + THREAD_AFFINITY_POLICY, (thread_policy_t)&policy, + THREAD_AFFINITY_POLICY_COUNT); + if (rc != KERN_SUCCESS) { + jclass ex = (*env)->FindClass(env, "java/lang/RuntimeException"); + char msg[100]; + sprintf(msg, "Bad return value from thread_policy_set: %d", rc); + (*env)->ThrowNew(env, ex, msg); + } +} 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 92e96c6a3..000000000 --- a/affinity/src/main/java/java/lang/ThreadLifecycleListener.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package 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 e23060712..000000000 --- a/affinity/src/main/java/java/lang/ThreadTrackingGroup.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package 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/AffinitySupport.java b/affinity/src/main/java/net/openhft/affinity/Affinity.java similarity index 70% rename from affinity/src/main/java/net/openhft/affinity/AffinitySupport.java rename to affinity/src/main/java/net/openhft/affinity/Affinity.java index 0108a1289..8dadd8a15 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinitySupport.java +++ b/affinity/src/main/java/net/openhft/affinity/Affinity.java @@ -1,23 +1,9 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; +import com.sun.jna.Native; import net.openhft.affinity.impl.*; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -26,31 +12,40 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; +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 AffinitySupport { - ; +public enum Affinity { + ; // none + static final Logger LOGGER = LoggerFactory.getLogger(Affinity.class); @NotNull private static final IAffinity AFFINITY_IMPL; - private static final Logger LOGGER = LoggerFactory.getLogger(AffinitySupport.class); private static Boolean JNAAvailable; - + static { String osName = System.getProperty("os.name"); if (osName.contains("Win") && isWindowsJNAAffinityUsable()) { LOGGER.trace("Using Windows JNA-based affinity control implementation"); AFFINITY_IMPL = WindowsJNAAffinity.INSTANCE; + } else if (osName.contains("x")) { - if(osName.startsWith("Linux") && isLinuxJNAAffinityUsable()) { + /*if (osName.startsWith("Linux") && NativeAffinity.LOADED) { + LOGGER.trace("Using Linux JNI-based affinity control implementation"); + AFFINITY_IMPL = NativeAffinity.INSTANCE; + } else*/ + if (osName.startsWith("Linux") && isLinuxJNAAffinityUsable()) { LOGGER.trace("Using Linux JNA-based affinity control implementation"); AFFINITY_IMPL = LinuxJNAAffinity.INSTANCE; - } else if(isPosixJNAAffinityUsable()) { + + } else if (isPosixJNAAffinityUsable()) { LOGGER.trace("Using Posix JNA-based affinity control implementation"); AFFINITY_IMPL = PosixJNAAffinity.INSTANCE; + } else { LOGGER.info("Using dummy affinity control implementation"); AFFINITY_IMPL = NullAffinity.INSTANCE; @@ -58,9 +53,11 @@ public enum AffinitySupport { } else if (osName.contains("Mac") && isMacJNAAffinityUsable()) { LOGGER.trace("Using MAC OSX JNA-based thread id implementation"); AFFINITY_IMPL = OSXJNAAffinity.INSTANCE; + } else if (osName.contains("SunOS") && isSolarisJNAAffinityUsable()) { LOGGER.trace("Using Solaris JNA-based thread id implementation"); AFFINITY_IMPL = SolarisJNAAffinity.INSTANCE; + } else { LOGGER.info("Using dummy affinity control implementation"); AFFINITY_IMPL = NullAffinity.INSTANCE; @@ -70,7 +67,7 @@ public enum AffinitySupport { public static IAffinity getAffinityImpl() { return AFFINITY_IMPL; } - + private static boolean isWindowsJNAAffinityUsable() { if (isJNAAvailable()) { try { @@ -116,8 +113,9 @@ private static boolean isLinuxJNAAffinityUsable() { private static boolean isMacJNAAffinityUsable() { if (isJNAAvailable()) { 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; } } @@ -125,6 +123,7 @@ private static boolean isMacJNAAffinityUsable() { private static boolean isSolarisJNAAffinityUsable() { if (isJNAAvailable()) { return true; + } else { LOGGER.warn("Solaris JNA-based affinity not usable due to JNA not being available!"); return false; @@ -139,14 +138,20 @@ private static void logThrowable(Throwable t, String description) { LOGGER.warn(sw.toString()); } - public static long getAffinity() { + public static BitSet getAffinity() { return AFFINITY_IMPL.getAffinity(); } - public static void setAffinity(final long affinity) { + 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 int getCpu() { return AFFINITY_IMPL.getCpu(); } @@ -155,48 +160,54 @@ public static int getThreadId() { return AFFINITY_IMPL.getThreadId(); } - public static boolean isJNAAvailable() { - if (JNAAvailable == null) - try { - Class.forName("com.sun.jna.Platform"); - JNAAvailable = true; - } catch (ClassNotFoundException ignored) { - JNAAvailable = false; - } - return JNAAvailable; - } - public static void setThreadId() { try { - int threadId = getThreadId(); + int threadId = Affinity.getThreadId(); final Field tid = Thread.class.getDeclaredField("tid"); tid.setAccessible(true); final Thread thread = Thread.currentThread(); tid.setLong(thread, threadId); - LOGGER.info("Set {} to thread id {}", thread.getName(), threadId); + Affinity.LOGGER.info("Set {} to thread id {}", thread.getName(), threadId); } catch (Exception e) { throw new IllegalStateException(e); } } + public static boolean isJNAAvailable() { + 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 7a2c7ba25..cf0ed4ca0 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityLock.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityLock.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; import net.openhft.affinity.impl.NoCpuLayout; @@ -25,40 +10,52 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +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 { - private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLock.class); - +public class AffinityLock implements Closeable { // 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 long BASE_AFFINITY = AffinitySupport.getAffinity(); - public static final long 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 { */ 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,17 +102,29 @@ public static CpuLayout cpuLayout() { return LOCK_INVENTORY.getCpuLayout(); } - private static long getReservedAffinity0() { + private static BitSet getReservedAffinity0() { String reservedAffinity = System.getProperty(AFFINITY_RESERVED); - if (reservedAffinity == null || reservedAffinity.trim().isEmpty()) { - long reserverable = ((1 << PROCESSORS) - 1) ^ BASE_AFFINITY; - if (reserverable == 0 && PROCESSORS > 1) { - LoggerFactory.getLogger(AffinityLock.class).info("No isolated CPUs found, so assuming CPUs 1 to {} available.", (PROCESSORS - 1)); - return ((1 << PROCESSORS) - 2); + if (BASE_AFFINITY != null && (reservedAffinity == null || reservedAffinity.trim().isEmpty())) { + BitSet reserverable = new BitSet(PROCESSORS); + 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; } - return 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); } /** @@ -125,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. */ @@ -137,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); } @@ -176,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. @@ -204,19 +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) { + BitSet affinity = new BitSet(); + affinity.set(cpuId, true); + Affinity.setAffinity(affinity); } - if (cpuId >= 0) - AffinitySupport.setAffinity(1L << cpuId); } - 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; @@ -228,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. @@ -244,13 +432,24 @@ 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 + 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(); @@ -263,6 +462,10 @@ public int cpuId() { return cpuId; } + public int cpuId2() { + return cpuId2; + } + /** * @return Was a cpu found to bind this lock to. */ @@ -290,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 65b9da457..65ee0cbf9 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityStrategies.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityStrategies.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; /** @@ -24,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 04a94e771..400c6d3c6 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityStrategy.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityStrategy.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; /** diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java index 75048a39d..6e1d67e5a 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; import org.jetbrains.annotations.NotNull; @@ -54,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 b7f7e49f2..85efa3279 100644 --- a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java +++ b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java @@ -1,60 +1,132 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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 ccfefc07e..0cbdd6412 100644 --- a/affinity/src/main/java/net/openhft/affinity/CpuLayout.java +++ b/affinity/src/main/java/net/openhft/affinity/CpuLayout.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; /** @@ -50,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 09fcb8fd2..c473a5400 100644 --- a/affinity/src/main/java/net/openhft/affinity/IAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/IAffinity.java @@ -1,23 +1,10 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; +import java.util.BitSet; + /** * Implementation interface * @@ -26,14 +13,14 @@ */ public interface IAffinity { /** - * @return returns affinity mask for current thread, or -1 if unknown + * @return returns affinity mask for current thread, or null if unknown */ - long getAffinity(); + BitSet getAffinity(); /** * @param affinity sets affinity mask of current thread to specified value */ - void setAffinity(final long affinity); + void setAffinity(final BitSet affinity); /** * @return the current cpu id, or -1 if unknown. 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 1f24cb6f4..b54615219 100644 --- a/affinity/src/main/java/net/openhft/affinity/LockInventory.java +++ b/affinity/src/main/java/net/openhft/affinity/LockInventory.java @@ -1,49 +1,84 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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); - + /** + * 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 CpuLayout cpuLayout; - /** * The lock belonging to each logical core. 1-to-1 relationship */ private AffinityLock[] logicalCoreLocks; + public LockInventory(CpuLayout cpuLayout) { + set(cpuLayout); + } + + public static String dumpLocks(@NotNull AffinityLock[] locks) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < locks.length; i++) { + AffinityLock al = locks[i]; + sb.append(i).append(": "); + 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; + } + /** - * The locks belonging to physical cores. Since a physical core can host multiple logical cores - * the relationship is one to many. + * 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 final NavigableMap physicalCoreLocks = new TreeMap(); + 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; - public LockInventory(CpuLayout cpuLayout) { - set(cpuLayout); + } catch (IOException e) { + LOGGER.info("Error occurred acquiring lock, trying another {}", String.valueOf(e)); + } + return false; } public final synchronized CpuLayout getCpuLayout() { @@ -56,11 +91,12 @@ public final synchronized void set(CpuLayout cpuLayout) { } reset(cpuLayout); for (int i = 0; i < cpuLayout.cpus(); i++) { - boolean base = ((AffinityLock.BASE_AFFINITY >> i) & 1) != 0; - boolean reservable = ((AffinityLock.RESERVED_AFFINITY >> i) & 1) != 0; - - LOGGER.trace("cpu " + i + " base={} reservable= {}", i, base, reservable); - AffinityLock lock = logicalCoreLocks[i] = newLock(i, base, reservable); + final boolean base = AffinityLock.BASE_AFFINITY.get(i); + final boolean reservable = AffinityLock.RESERVED_AFFINITY.get(i); + 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); @@ -70,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) { @@ -95,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(); + } } } @@ -119,6 +228,7 @@ public final synchronized void bindWholeCore(int logicalCoreID) { for (AffinityLock al : physicalCoreLocks.get(core)) { if (al.isBound() && al.assignedThread != null && al.assignedThread.isAlive()) { LOGGER.warn("cpu {} already bound to {}", al.cpuId(), al.assignedThread); + } else { al.bound = true; al.assignedThread = Thread.currentThread(); @@ -137,29 +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."); } } - AffinitySupport.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) { @@ -172,14 +279,16 @@ private int toPhysicalCore(int layoutId) { return cpuLayout.socketId(layoutId) * cpuLayout.coresPerSocket() + cpuLayout.coreId(layoutId); } - public static String dumpLocks(@NotNull AffinityLock[] locks) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < locks.length; i++) { - AffinityLock al = locks[i]; - sb.append(i).append(": "); - sb.append(al.toString()); - sb.append('\n'); - } - return sb.toString(); + 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); } -} \ No newline at end of file +} 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 549e90394..000000000 --- a/affinity/src/main/java/net/openhft/affinity/NonForkingAffinityLock.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.openhft.affinity; - -import net.openhft.affinity.impl.NoCpuLayout; -import org.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 39e547e7c..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,3 +1,6 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ package net.openhft.affinity.impl; import com.sun.jna.*; @@ -5,16 +8,164 @@ import org.jetbrains.annotations.NotNull; 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; @@ -27,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)); } @@ -75,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; @@ -93,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)); } @@ -105,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); } } @@ -148,144 +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 { - static final 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 long 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; - if(Platform.is64Bit()) { - cpuset.__bits[0].setValue(affinity); - } else { - cpuset.__bits[0].setValue(affinity & 0xFFFFFFFFL); - cpuset.__bits[1].setValue((affinity >>> 32) & 0xFFFFFFFFL); - } - try { - if(lib.sched_setaffinity(0, size, cpuset) != 0) { - throw new IllegalStateException("sched_setaffinity(0, " + size + - ", 0x" + Long.toHexString(affinity) + " failed; errno=" + Native.getLastError()); - } - } catch (LastErrorException e) { - throw new IllegalStateException("sched_setaffinity(0, " + size + - ", 0x" + Long.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 f0c33eb10..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,25 +1,65 @@ +/* + * 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.util.BitSet; + 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 = {}; + + private static final String OS = System.getProperty("os.name").toLowerCase(); + private static final boolean IS_LINUX = OS.startsWith("linux"); + + static { + int pid = -1; + try { + pid = LinuxHelper.getpid(); + } catch (NoClassDefFoundError | Exception ignored) { + } + PROCESS_ID = pid; + } + + 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; + } + + private final ThreadLocal THREAD_ID = new ThreadLocal<>(); - // TODO: FIXME!!! CHANGE IAffinity TO SUPPORT PLATFORMS WITH 64+ CORES FIXME!!! @Override - public long getAffinity() { + public BitSet getAffinity() { final LinuxHelper.cpu_set_t cpuset = LinuxHelper.sched_getaffinity(); - return cpuset.__bits[0].longValue(); + + 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 long affinity) { + public void setAffinity(final BitSet affinity) { LinuxHelper.sched_setaffinity(affinity); } @@ -28,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(); @@ -54,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 cbdeae6d6..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,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity.impl; import net.openhft.affinity.CpuLayout; @@ -65,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 573b25856..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,28 +1,13 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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; /** * @author peter.lawrey @@ -32,13 +17,13 @@ public enum NullAffinity implements IAffinity { private static final Logger LOGGER = LoggerFactory.getLogger(NullAffinity.class); @Override - public long getAffinity() { - return -1; + public BitSet getAffinity() { + return new BitSet(); } @Override - public void setAffinity(final long affinity) { - LOGGER.trace("unable to set mask to {} as the JNIa nd JNA libraries and not loaded", Long.toHexString(affinity)); + public void setAffinity(final BitSet affinity) { + LOGGER.trace("unable to set mask to {} as the JNI and JNA libraries not loaded", Utilities.toHexString(affinity)); } @Override @@ -48,13 +33,11 @@ public int getCpu() { @Override public int getProcessId() { - final String name = ManagementFactory.getRuntimeMXBean().getName(); - return Integer.parseInt(name.split("@")[0]); + return Utilities.currentProcessId(); } @Override public int getThreadId() { throw new UnsupportedOperationException(); } - } 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 b86f91696..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,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity.impl; import com.sun.jna.LastErrorException; @@ -25,11 +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 { @@ -38,13 +24,13 @@ public enum OSXJNAAffinity implements IAffinity { private final ThreadLocal THREAD_ID = new ThreadLocal<>(); @Override - public long getAffinity() { - return -1; + public BitSet getAffinity() { + return new BitSet(); } @Override - public void setAffinity(final long affinity) { - LOGGER.trace("unable to set mask to {} as the JNIa nd JNA libraries and not loaded", Long.toHexString(affinity)); + public void setAffinity(final BitSet 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 89948b885..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,30 +1,19 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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; import org.slf4j.LoggerFactory; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.BitSet; + /** * Implementation of {@link IAffinity} based on JNA call of * sched_setaffinity(3)/sched_getaffinity(3) from 'c' library. Applicable for most @@ -40,52 +29,99 @@ 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 long getAffinity() { + public BitSet getAffinity() { final CLibrary lib = CLibrary.INSTANCE; - // TODO where are systems with 64+ cores... - final LongByReference cpuset = new LongByReference(0L); + final int procs = Runtime.getRuntime().availableProcessors(); + + final int cpuSetSizeInLongs = (procs + 63) / 64; + final int cpuSetSizeInBytes = cpuSetSizeInLongs * 8; + final Memory cpusetArray = new Memory(cpuSetSizeInBytes); + final PointerByReference cpuset = new PointerByReference(cpusetArray); try { - final int ret = lib.sched_getaffinity(0, Long.SIZE / 8, cpuset); - if (ret < 0) - throw new IllegalStateException("sched_getaffinity((" + Long.SIZE / 8 + ") , &(" + cpuset + ") ) return " + ret); - return cpuset.getValue(); + final int ret = lib.sched_getaffinity(0, cpuSetSizeInBytes, cpuset); + 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) - throw new IllegalStateException("sched_getaffinity((" + Long.SIZE / 8 + ") , &(" + cpuset + ") ) errorNo=" + e.getErrorCode(), 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 { 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); - return cpuset32.getValue() & 0xFFFFFFFFL; + } + long[] longs = new long[1]; + longs[0] = cpuset32.getValue() & 0xFFFFFFFFL; + return BitSet.valueOf(longs); } catch (LastErrorException e) { throw new IllegalStateException("sched_getaffinity((" + Integer.SIZE / 8 + ") , &(" + cpuset32 + ") ) errorNo=" + e.getErrorCode(), e); } } @Override - public void setAffinity(final long affinity) { + public void setAffinity(final BitSet affinity) { int procs = Runtime.getRuntime().availableProcessors(); - if (procs < 64 && (affinity & ((1L << procs) - 1)) == 0) + if (affinity.isEmpty()) { throw new IllegalArgumentException("Cannot set zero affinity"); + } + final CLibrary lib = CLibrary.INSTANCE; + byte[] buff = affinity.toByteArray(); + final int cpuSetSizeInBytes = buff.length; + final Memory cpusetArray = new Memory(cpuSetSizeInBytes); try { - //fixme: where are systems with more then 64 cores... - final int ret = lib.sched_setaffinity(0, Long.SIZE / 8, new LongByReference(affinity)); + cpusetArray.write(0, buff, 0, buff.length); + final int ret = lib.sched_setaffinity(0, cpuSetSizeInBytes, new PointerByReference(cpusetArray)); if (ret < 0) { - throw new IllegalStateException("sched_setaffinity((" + Long.SIZE / 8 + ") , &(" + affinity + ") ) return " + ret); + throw new IllegalStateException("sched_setaffinity((" + cpuSetSizeInBytes + ") , &(" + affinity + ") ) return " + ret); } } catch (LastErrorException e) { - if (e.getErrorCode() != 22 || (affinity & 0xFFFFFFFFL) != affinity) - throw new IllegalStateException("sched_setaffinity((" + Long.SIZE / 8 + ") , &(" + affinity + ") ) errorNo=" + e.getErrorCode(), 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) { + throw new IllegalArgumentException("Cannot set zero affinity"); } - if (procs < 32 && (affinity & ((1L << procs) - 1)) == 0) - throw new IllegalArgumentException("Cannot set zero affinity for 32-bit set affinity"); final IntByReference cpuset32 = new IntByReference(0); - cpuset32.setValue((int) affinity); + cpuset32.setValue(value); try { final int ret = lib.sched_setaffinity(0, Integer.SIZE / 8, cpuset32); if (ret < 0) @@ -106,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); @@ -121,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)); @@ -151,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, @@ -193,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; @@ -202,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 0885f5e52..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,24 +1,8 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity.impl; - import com.sun.jna.LastErrorException; import com.sun.jna.Library; import com.sun.jna.Native; @@ -26,11 +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 { @@ -39,13 +24,13 @@ public enum SolarisJNAAffinity implements IAffinity { private final ThreadLocal THREAD_ID = new ThreadLocal<>(); @Override - public long getAffinity() { - return -1; + public BitSet getAffinity() { + return new BitSet(); } @Override - public void setAffinity(final long affinity) { - LOGGER.trace("unable to set mask to {} as the JNIa nd JNA libraries and not loaded", Long.toHexString(affinity)); + public void setAffinity(final BitSet affinity) { + LOGGER.trace("unable to set mask to {} as the JNI and JNA libraries not loaded", Utilities.toHexString(affinity)); } @Override @@ -55,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 @@ -72,10 +56,8 @@ 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 new file mode 100644 index 000000000..babf6b360 --- /dev/null +++ b/affinity/src/main/java/net/openhft/affinity/impl/Utilities.java @@ -0,0 +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 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) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(out); + final long[] longs = set.toLongArray(); + 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) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(out); + final long[] longs = set.toLongArray(); + 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 13781d222..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,28 +1,15 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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; @@ -31,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; @@ -41,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); @@ -61,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()); } } @@ -79,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; @@ -114,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()) { @@ -176,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() { @@ -197,9 +197,8 @@ public boolean equals(@Nullable Object o) { if (coresPerSocket != that.coresPerSocket) return false; if (sockets != that.sockets) return false; if (threadsPerCore != that.threadsPerCore) return false; - if (!cpuDetails.equals(that.cpuDetails)) return false; + return cpuDetails.equals(that.cpuDetails); - return true; } @Override @@ -242,9 +241,8 @@ public boolean equals(@Nullable Object o) { if (coreId != cpuInfo.coreId) return false; if (socketId != cpuInfo.socketId) return false; - if (threadId != cpuInfo.threadId) return false; + return threadId == cpuInfo.threadId; - return true; } @Override 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 3a81ad37c..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,3 +1,6 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ package net.openhft.affinity.impl; public class VersionHelper { @@ -13,11 +16,12 @@ 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; release = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; + } else { major = minor = release = 0; } @@ -28,11 +32,12 @@ 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; + } else { return false; } @@ -42,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 0e6f8f720..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,34 +1,20 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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; +import java.util.BitSet; + /** * Implementation of {@link net.openhft.affinity.IAffinity} based on JNA call of * sched_SetThreadAffinityMask/GetProcessAffinityMask from Windows 'kernel32' library. Applicable for @@ -41,37 +27,86 @@ public enum WindowsJNAAffinity implements IAffinity { INSTANCE; public static final boolean LOADED; private static final Logger LOGGER = LoggerFactory.getLogger(WindowsJNAAffinity.class); - private final ThreadLocal THREAD_ID = new ThreadLocal<>(); + private static final ThreadLocal currentAffinity = new ThreadLocal<>(); - @Override - public long getAffinity() { - final CLibrary lib = CLibrary.INSTANCE; - final LongByReference cpuset1 = new LongByReference(0); - final LongByReference cpuset2 = new LongByReference(0); + static { + boolean loaded = false; try { + INSTANCE.getAffinity(); + loaded = true; + } catch (UnsatisfiedLinkError e) { + LOGGER.warn("Unable to load jna library", e); + } + LOADED = loaded; + } - final int ret = lib.GetProcessAffinityMask(-1, cpuset1, cpuset2); - if (ret < 0) - throw new IllegalStateException("GetProcessAffinityMask(( -1 ), &(" + cpuset1 + "), &(" + cpuset2 + ") ) return " + ret); + private final ThreadLocal THREAD_ID = new ThreadLocal<>(); - return cpuset1.getValue(); - } catch (Exception e) { - e.printStackTrace(); - } - return 0; + @Override + public BitSet getAffinity() { + BitSet bitSet = currentAffinity.get(); + if (bitSet != null) + return bitSet; + BitSet longs = getAffinity0(); + return longs != null ? longs : new BitSet(); } @Override - public void setAffinity(final long affinity) { + public void setAffinity(final BitSet affinity) { final CLibrary lib = CLibrary.INSTANCE; - WinDef.DWORD aff = new WinDef.DWORD(affinity); + WinDef.DWORD aff; + long[] longs = affinity.toLongArray(); + switch (longs.length) { + case 0: + aff = new WinDef.DWORD(0); + break; + case 1: + aff = new WinDef.DWORD(longs[0]); + break; + default: + throw new IllegalArgumentException("Windows API does not support more than 64 CPUs for thread affinity"); + } + int pid = getTid(); try { - lib.SetThreadAffinityMask(pid, aff); + 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() { @@ -106,23 +141,12 @@ 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; } - - 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/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 new file mode 100644 index 000000000..3a9ab33d3 --- /dev/null +++ b/affinity/src/main/java/net/openhft/ticker/ITicker.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.ticker; + +/** + * 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(); + + long ticks(); + + long toNanos(long ticks); + + double toMicros(double ticks); +} diff --git a/affinity/src/main/java/net/openhft/ticker/Ticker.java b/affinity/src/main/java/net/openhft/ticker/Ticker.java new file mode 100644 index 000000000..4d057cf2a --- /dev/null +++ b/affinity/src/main/java/net/openhft/ticker/Ticker.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.ticker; + +import net.openhft.ticker.impl.JNIClock; +import net.openhft.ticker.impl.SystemClock; + +/** + * Static factory for available {@link ITicker} interface implementation + * + * @author Peter.Lawrey + */ +public final class Ticker { + public static final ITicker INSTANCE; + + static { + if (JNIClock.LOADED) { + INSTANCE = JNIClock.INSTANCE; + } else { + INSTANCE = SystemClock.INSTANCE; + } + } + + private Ticker() { + throw new InstantiationError("Must not instantiate this class"); + } + + /** + * @return The current value of the system timer, in nanoseconds. + */ + public static long ticks() { + return INSTANCE.ticks(); + } + + public static long nanoTime() { + return toNanos(ticks()); + } + + public static long toNanos(long ticks) { + return INSTANCE.toNanos(ticks); + } + + public static double toMicros(long ticks) { + return INSTANCE.toMicros(ticks); + } +} diff --git a/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java b/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java new file mode 100644 index 000000000..c50c3826c --- /dev/null +++ b/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java @@ -0,0 +1,93 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.ticker.impl; + +import net.openhft.ticker.ITicker; +import software.chronicle.enterprise.internals.impl.NativeAffinity; + +import java.util.logging.Logger; + +/** + * JNI-based implementation, trying to use rdtsc() system call + * to access the most precise timer available + * + * @author cheremin + * @since 29.12.11, 18:56 + */ +public enum JNIClock implements ITicker { + INSTANCE; + + public static final boolean LOADED; + private static final Logger LOGGER = Logger.getLogger(JNIClock.class.getName()); + private static final int FACTOR_BITS = 17; + private static final long START; + private static long RDTSC_FACTOR = 1 << FACTOR_BITS; + private static double RDTSC_MICRO_FACTOR = 1e-3; + private static long CPU_FREQUENCY = 1000; + + static { + boolean loaded; + long start; + try { + // ensure it is loaded. + NativeAffinity.INSTANCE.getCpu(); + + estimateFrequency(50); + estimateFrequency(200); + LOGGER.info("Estimated clock frequency was " + CPU_FREQUENCY + " MHz"); + start = rdtsc0(); + loaded = true; + } catch (UnsatisfiedLinkError ule) { + LOGGER.fine("Unable to find libCEInternals in [" + System.getProperty("java.library.path") + "] " + ule); + start = 0; + loaded = false; + } + LOADED = loaded; + START = start; + } + + 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 (System.nanoTime() == start) { + } + + long end = start + factor * 1000000L; + final long start0 = rdtsc0(); + while ((now = System.nanoTime()) < end) { + } + long end0 = rdtsc0(); + end = now; + + RDTSC_FACTOR = ((end - start) << FACTOR_BITS) / (end0 - start0) - 1; + RDTSC_MICRO_FACTOR = 1e-3 * (end - start) / (end0 - start0); + CPU_FREQUENCY = (end0 - start0 + 1) * 1000 / (end - start); + } + + native static long rdtsc0(); + + public long nanoTime() { + return tscToNano(rdtsc0() - START); + } + + @Override + public long ticks() { + return rdtsc0(); + } + + @Override + public long toNanos(long ticks) { + return tscToNano(ticks); + } + + @Override + public double toMicros(double ticks) { + return ticks * RDTSC_MICRO_FACTOR; + } +} diff --git a/affinity/src/main/java/net/openhft/ticker/impl/SystemClock.java b/affinity/src/main/java/net/openhft/ticker/impl/SystemClock.java new file mode 100644 index 000000000..cbbd3545e --- /dev/null +++ b/affinity/src/main/java/net/openhft/ticker/impl/SystemClock.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package net.openhft.ticker.impl; + +import net.openhft.ticker.ITicker; + +/** + * Default implementation, use plain {@link System#nanoTime()} + * + * @author cheremin + * @since 29.12.11, 18:54 + */ +public enum SystemClock implements ITicker { + INSTANCE; + + @Override + public long nanoTime() { + return System.nanoTime(); + } + + @Override + public long ticks() { + return nanoTime(); + } + + @Override + public long toNanos(long ticks) { + return ticks; + } + + @Override + public double toMicros(double ticks) { + return ticks / 1e3; + } +} 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 new file mode 100644 index 000000000..aa501a8f0 --- /dev/null +++ b/affinity/src/main/java/software/chronicle/enterprise/internals/impl/NativeAffinity.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2025 chronicle.software; SPDX-License-Identifier: Apache-2.0 + */ +package software.chronicle.enterprise.internals.impl; + +import net.openhft.affinity.IAffinity; + +import java.util.BitSet; + +public enum NativeAffinity implements IAffinity { + INSTANCE; + + public static final boolean LOADED; + + static { + LOADED = loadAffinityNativeLibrary(); + } + + private native static byte[] getAffinity0(); + + private native static void setAffinity0(byte[] affinity); + + private native static int getCpu0(); + + private native static int getProcessId0(); + + private native static int getThreadId0(); + + private native static long rdtsc0(); + + @SuppressWarnings("restricted") + private static boolean loadAffinityNativeLibrary() { + try { + System.loadLibrary("CEInternals"); + return true; + } catch (UnsatisfiedLinkError e) { + return false; + } + } + + @Override + public BitSet getAffinity() { + final byte[] buff = getAffinity0(); + if (buff == null) { + return null; + } + return BitSet.valueOf(buff); + } + + @Override + public void setAffinity(BitSet affinity) { + setAffinity0(affinity.toByteArray()); + } + + @Override + public int getCpu() { + return getCpu0(); + } + + @Override + public int getProcessId() { + return getProcessId0(); + } + + @Override + public int getThreadId() { + return getThreadId0(); + } +} diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java b/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java index 4166ae1ac..b1ed7effe 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockBindMain.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; import static net.openhft.affinity.AffinityStrategies.*; @@ -23,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 { @@ -62,6 +51,7 @@ public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } finally { affinityLock.release(); } 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 5de35d1f2..cf7388ec6 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityLockMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockMain.java @@ -1,27 +1,16 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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 { @@ -43,6 +32,7 @@ public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } finally { al.release(); } 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 94882f733..4e8ff5519 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java @@ -1,56 +1,57 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; +import net.openhft.affinity.impl.Utilities; import net.openhft.affinity.impl.VanillaCpuLayout; -import org.jetbrains.annotations.NotNull; +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 net.openhft.affinity.AffinityLock.acquireLock; -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(); @@ -59,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" + @@ -80,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" + @@ -103,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); @@ -119,27 +120,45 @@ public void dumpLocksCoreDuo() throws IOException { @Test public void assignReleaseThread() throws IOException { - if (AffinityLock.RESERVED_AFFINITY == 0) { + if (AffinityLock.RESERVED_AFFINITY.isEmpty()) { System.out.println("Cannot run affinity test as no threads gave been reserved."); System.out.println("Use isolcpus= in grub.conf or use -D" + AffinityLock.AFFINITY_RESERVED + "={hex mask}"); return; + } else if (!new File("/proc/cpuinfo").exists()) { 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, AffinitySupport.getAffinity()); + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); AffinityLock al = AffinityLock.acquireLock(); - assertEquals(1, Long.bitCount(AffinitySupport.getAffinity())); + assertEquals(1, Affinity.getAffinity().cardinality()); al.release(); - assertEquals(AffinityLock.BASE_AFFINITY, AffinitySupport.getAffinity()); + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); - assertEquals(AffinityLock.BASE_AFFINITY, AffinitySupport.getAffinity()); + assertEquals(AffinityLock.BASE_AFFINITY, Affinity.getAffinity()); AffinityLock al2 = AffinityLock.acquireCore(); - assertEquals(1, Long.bitCount(AffinitySupport.getAffinity())); + assertEquals(1, Affinity.getAffinity().cardinality()); al2.release(); - assertEquals(AffinityLock.BASE_AFFINITY, AffinitySupport.getAffinity()); + 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 @@ -154,12 +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(); @@ -167,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, @@ -184,36 +206,165 @@ public void testIssue19() { @Test public void testGettid() { - System.out.println("cpu= " + AffinitySupport.getCpu()); + System.out.println("cpu= " + Affinity.getCpu()); } @Test public void testAffinity() throws InterruptedException { - // System.out.println("Started"); logger.info("Started"); displayStatus(); - final AffinityLock al = acquireLock(); - System.out.println("Main locked"); - displayStatus(); - Thread t = new Thread(new Runnable() { - @Override - public void run() { - AffinityLock al2 = al.acquireLock(AffinityStrategies.ANY); + try (AffinityLock al = AffinityLock.acquireLock()) { + System.out.println("Main locked"); + displayStatus(); + 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(); - al.release(); + }); + t.start(); + t.join(); + System.out.println("Thread-0 unlocked"); + displayStatus(); + } 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 " + AffinitySupport.getCpu() + "\n" + AffinityLock.dumpLocks()); + 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 784714b04..d7cf5b35d 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinitySupportMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinitySupportMain.java @@ -1,27 +1,16 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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 { @@ -40,6 +29,7 @@ public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } finally { al.release(); } diff --git a/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java index 35292591e..3dbd15396 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; import java.util.concurrent.Callable; @@ -28,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 edea79413..9e85c0790 100644 --- a/affinity/src/test/java/net/openhft/affinity/InterrupedThread.java +++ b/affinity/src/test/java/net/openhft/affinity/InterrupedThread.java @@ -1,21 +1,6 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity; /** @@ -26,6 +11,7 @@ public void run() { try { Thread.sleep(Integer.MAX_VALUE); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } } 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 b1ef31b05..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,87 +1,77 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 net.openhft.affinity.impl; +import net.openhft.affinity.BaseAffinityTest; import net.openhft.affinity.IAffinity; import org.junit.After; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import java.util.BitSet; + +import static org.junit.Assert.*; /** * @author cheremin * @since 29.12.11, 20:25 */ -public abstract class AbstractAffinityImplTest { +public abstract class AbstractAffinityImplTest extends BaseAffinityTest { + + private static final int CORES = Runtime.getRuntime().availableProcessors(); + private static final BitSet CORES_MASK = new BitSet(CORES); - protected static final int CORES = Runtime.getRuntime().availableProcessors(); + static { + CORES_MASK.set(0, CORES, true); + } - public abstract IAffinity getImpl(); + protected abstract IAffinity getImpl(); @Test - public void getAffinityCompletesGracefully() throws Exception { + public void getAffinityCompletesGracefully() { getImpl().getAffinity(); } @Test - public void getAffinityReturnsValidValue() throws Exception { - final long affinity = getImpl().getAffinity(); - assertTrue( - "Affinity mask " + affinity + " must be >0", - affinity > 0 - ); + public void getAffinityReturnsValidValue() { + final BitSet affinity = getImpl().getAffinity(); + assertFalse("Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", affinity.isEmpty()); final long allCoresMask = (1L << CORES) - 1; assertTrue( - "Affinity mask " + affinity + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", - affinity <= allCoresMask + "Affinity mask " + Utilities.toBinaryString(affinity) + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", + affinity.length() <= CORES_MASK.length() ); } @Test - public void setAffinityCompletesGracefully() throws Exception { - getImpl().setAffinity(1); + public void setAffinityCompletesGracefully() { + BitSet affinity = new BitSet(1); + affinity.set(0, true); + getImpl().setAffinity(affinity); } @Test - public void getAffinityReturnsValuePreviouslySet() throws Exception { + public void getAffinityReturnsValuePreviouslySet() { final IAffinity impl = getImpl(); - final int cores = CORES; - for (int core = 0; core < cores; core++) { - final long mask = (1L << core); + for (int core = 0; core < CORES; core++) { + final BitSet mask = new BitSet(); + mask.set(core, true); getAffinityReturnsValuePreviouslySet(impl, mask); } } private void getAffinityReturnsValuePreviouslySet(final IAffinity impl, - final long mask) throws Exception { + final BitSet mask) { impl.setAffinity(mask); - final long _mask = impl.getAffinity(); + final BitSet _mask = impl.getAffinity(); assertEquals(mask, _mask); } @After - public void tearDown() throws Exception { - final long anyCore = (1L << CORES) - 1; + public void tearDown() { try { - getImpl().setAffinity(anyCore); + getImpl().setAffinity(CORES_MASK); } catch (Exception e) { e.printStackTrace(); } 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 new file mode 100644 index 000000000..2b9cbb73a --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/NativeAffinityImpTest.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.Affinity; +import net.openhft.affinity.IAffinity; +import org.junit.Assume; +import org.junit.BeforeClass; +import org.junit.Test; +import software.chronicle.enterprise.internals.impl.NativeAffinity; + +import static org.junit.Assert.assertTrue; + +/* + * Created by andre on 22/06/15. + */ +public class NativeAffinityImpTest extends AbstractAffinityImplTest { + @BeforeClass + public static void checkJniLibraryPresent() { + Assume.assumeTrue(NativeAffinity.LOADED); + Assume.assumeTrue("linux".equalsIgnoreCase(System.getProperty("os.name"))); + } + + @Override + public IAffinity getImpl() { + return NativeAffinity.INSTANCE; + } + + @Test + public void testGettid() { + System.out.println("pid=" + getImpl().getProcessId()); + System.out.println("tid=" + getImpl().getThreadId()); + Affinity.setThreadId(); + + for (int j = 0; j < 3; j++) { + final int runs = 100000; + long tid = 0; + long time = 0; + for (int i = 0; i < runs; i++) { + long start = System.nanoTime(); + @SuppressWarnings("deprecation") + long tid0 = Thread.currentThread().getId(); + tid = tid0; + time += System.nanoTime() - start; + assertTrue(tid > 0); + assertTrue(tid < 1 << 16); + } + 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/PosixJNAAffinityTest.java b/affinity/src/test/java/net/openhft/affinity/impl/PosixJNAAffinityTest.java index 33c3d9f04..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,30 +1,15 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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; import org.junit.Test; import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; /** * @author peter.lawrey @@ -32,19 +17,20 @@ 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 public IAffinity getImpl() { - return AffinitySupport.getAffinityImpl(); + return Affinity.getAffinityImpl(); } @Test 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; @@ -52,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 4e4323396..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,34 +1,20 @@ /* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * 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 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 new file mode 100644 index 000000000..359c06e02 --- /dev/null +++ b/affinity/src/test/java/net/openhft/ticker/impl/JNIClockTest.java @@ -0,0 +1,68 @@ +/* + * 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 Lawrey on 13/07/15. + */ +public class JNIClockTest extends BaseAffinityTest { + + @Test + @Ignore("TODO Fix") + public void testNanoTime() throws InterruptedException { + for (int i = 0; i < 20000; i++) + System.nanoTime(); + Affinity.setAffinity(2); + + JNIClock instance = JNIClock.INSTANCE; + for (int i = 0; i < 50; i++) { + long start0 = System.nanoTime(); + long start1 = instance.ticks(); + Thread.sleep(10); + long time0 = System.nanoTime(); + long time1 = instance.ticks(); + if (i > 1) { + assertEquals(10_100_000, time0 - start0, 100_000); + assertEquals(10_100_000, instance.toNanos(time1 - start1), 100_000); + assertEquals(instance.toNanos(time1 - start1) / 1e3, instance.toMicros(time1 - start1), 0.6); + } + } + } + + @Test + @Ignore("Long running") + public void testJitter() { + Affinity.setAffinity(2); + assertEquals(2, Affinity.getCpu()); + int samples = 100000, count = 0; + long[] time = new long[samples]; + long[] length = new long[samples]; + + JNIClock clock = JNIClock.INSTANCE; + long start = clock.ticks(), prev = start, prevJump = start; + for (int i = 0; i < 1000_000_000; i++) { + long now = clock.ticks(); + long delta = now - prev; + if (delta > 4_000) { + time[count] = now - prevJump; + prevJump = now; + length[count] = delta; + count++; + if (count >= samples) + break; + } + prev = now; + } + for (int i = 0; i < count; i++) { + System.out.println(((long) (clock.toMicros(time[i]) * 10)) / 10.0 + ", " + ((long) (clock.toMicros(length[i]) * 10) / 10.0)); + } + } +} 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 170fb1dc9..000000000 --- a/affinity/src/test/java/org/junit/Assert.java +++ /dev/null @@ -1,888 +0,0 @@ -/* - * Copyright 2014 Higher Frequency Trading - * - * http://www.higherfrequencytrading.com - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package 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; - } - if ((Math.abs(d1 - d2) <= delta)) { - return false; - } - - return true; - } - - /** - * 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 new file mode 100644 index 000000000..fa2bc7fba --- /dev/null +++ b/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java @@ -0,0 +1,122 @@ +/* + * 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.*; +import software.chronicle.enterprise.internals.impl.NativeAffinity; + +import java.util.BitSet; + +import static org.junit.Assert.*; + +/** + * @author peter.lawrey + */ +public class NativeAffinityTest 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(NativeAffinity.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 + @Ignore("TODO AFFINITY-25") + public void getAffinityReturnsValuePreviouslySet() { + String osName = System.getProperty("os.name"); + if (!osName.startsWith("Linux")) { + System.out.println("Skipping Linux tests"); + return; + } + final IAffinity impl = NativeAffinity.INSTANCE; + for (int core = 0; core < CORES; core++) { + final BitSet mask = new BitSet(); + mask.set(core, true); + getAffinityReturnsValuePreviouslySet(impl, mask); + } + } + + @Test + @Ignore("TODO AFFINITY-25") + public void JNAwithJNI() { + String osName = System.getProperty("os.name"); + if (!osName.startsWith("Linux")) { + System.out.println("Skipping Linux tests"); + return; + } + int nbits = Runtime.getRuntime().availableProcessors(); + BitSet affinity = new BitSet(nbits); + affinity.set(1); + NativeAffinity.INSTANCE.setAffinity(affinity); + BitSet affinity2 = LinuxJNAAffinity.INSTANCE.getAffinity(); + assertEquals(1, NativeAffinity.INSTANCE.getCpu()); + assertEquals(affinity, affinity2); + + affinity.clear(); + affinity.set(2); + LinuxJNAAffinity.INSTANCE.setAffinity(affinity); + BitSet affinity3 = NativeAffinity.INSTANCE.getAffinity(); + assertEquals(2, LinuxJNAAffinity.INSTANCE.getCpu()); + assertEquals(affinity, affinity3); + + affinity.set(0, nbits); + LinuxJNAAffinity.INSTANCE.setAffinity(affinity); + } + + @Test + public void showOtherIds() { + System.out.println("processId: " + NativeAffinity.INSTANCE.getProcessId()); + System.out.println("threadId: " + NativeAffinity.INSTANCE.getThreadId()); + System.out.println("cpu: " + NativeAffinity.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 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 e3f8fe675..9e617cf9c 100644 --- a/affinity/src/test/resources/i7.properties +++ b/affinity/src/test/resources/i7.properties @@ -1,13 +1,11 @@ # -# Copyright 2014 Higher Frequency Trading +# Copyright 2016-2025 chronicle.software # -# http://www.higherfrequencytrading.com -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -15,7 +13,7 @@ # 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 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 2479177aa..f88f45845 100644 --- a/pom.xml +++ b/pom.xml @@ -1,39 +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 - 2.1.6-SNAPSHOT + 2026.3-SNAPSHOT pom - Java Affinity Parent + OpenHFT/Java-Thread-Affinity Parent Java Thread Affinity library @@ -41,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 + +