diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f7f8b7f7d..000000000 --- a/LICENSE +++ /dev/null @@ -1,201 +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: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) 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 - - (d) 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 2014-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/LICENSE.adoc b/LICENSE.adoc new file mode 100644 index 000000000..eb12fcc48 --- /dev/null +++ b/LICENSE.adoc @@ -0,0 +1,14 @@ + +== 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 index 773c4ffa7..c51b9f438 100644 --- a/README.adoc +++ b/README.adoc @@ -17,6 +17,20 @@ 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 @@ -25,15 +39,16 @@ for working examples of how to use this library. === Dependencies -Java-Thread-Affinity will try to use https://github.com/java-native-access/jna[JNA] -to provide access to native thread-handling functions. JNA should be installed on +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 +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 @@ -41,20 +56,40 @@ for the artifacts `jna` and `jna-platform` in the project's `pom` file. === Installing JNA on Ubuntu - sudo apt-get install libjna-java +sudo apt-get install libjna-java === Installing JNA on CentOS - sudo yum install jna +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 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. +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. + +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. @@ -70,12 +105,28 @@ http://vanillajava.blogspot.co.uk/2013/07/micro-jitter-busy-waiting-and-binding. 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. +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 @@ -102,7 +153,8 @@ try (AffinityLock al = AffinityLock.acquireLock()) { You have further options such as === Acquiring a CORE lock for a thread -You can reserve a whole core. If you have hyper-threading enabled, this will use one CPU and leave it's twin CPU unused. +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()) { @@ -126,8 +178,22 @@ try (final AffinityLock al = AffinityLock.acquireLock()) { }); t.start(); } ----- -In this example, the library will prefer a free CPU on the same Socket as the first thread, otherwise it will pick any free CPU. +---- +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 @@ -149,18 +215,59 @@ The affinity of the process on start up is ---- 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 @@ -185,6 +292,25 @@ $ for i in "$(ls cpu-*)"; ---- +== 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] @@ -194,7 +320,9 @@ For an article on how much difference affinity can make and how to use it http:/ == Questions and Answers === Question: How to lock a specific cpuId -I am currently working on a project related to deadlock detection in multithreaded programs in java. We are trying to run threads on different processors and thus came across your github posts regarding the same. https://github.com/peter-lawrey/Java-Thread-Affinity/wiki/Getting-started +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 diff --git a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java index 9d1cfee02..80949898f 100644 --- a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java +++ b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiBundleTest.java @@ -64,7 +64,7 @@ public void checkInject() { public void checkBundleState() { final Bundle bundle = findBundle(context, "net.openhft.affinity"); assertNotNull(bundle); - assertEquals(bundle.getState(), Bundle.ACTIVE); + assertEquals(Bundle.ACTIVE, bundle.getState()); } @Test diff --git a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java index b50b0a8ce..a1e9fcc96 100644 --- a/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java +++ b/affinity-test/src/test/java/net/openhft/affinity/osgi/OSGiTestBase.java @@ -29,7 +29,7 @@ public class OSGiTestBase { public static Option workspaceBundle(String projectName) { String baseDir = System.getProperty("main.basedir"); - String bundleDir = null; + String bundleDir; bundleDir = String.format("%s/%s/target/classes", baseDir, projectName); if (new File(bundleDir).exists()) { diff --git a/affinity/pom.xml b/affinity/pom.xml index f634d4435..53a072427 100644 --- a/affinity/pom.xml +++ b/affinity/pom.xml @@ -110,6 +110,10 @@ make-c + + linux + !arm + !dontMake diff --git a/affinity/src/main/adoc/requirements.adoc b/affinity/src/main/adoc/requirements.adoc new file mode 100644 index 000000000..7cfacee5e --- /dev/null +++ b/affinity/src/main/adoc/requirements.adoc @@ -0,0 +1,297 @@ += 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/java/net/openhft/affinity/Affinity.java b/affinity/src/main/java/net/openhft/affinity/Affinity.java index 034602821..43bfb7ffe 100644 --- a/affinity/src/main/java/net/openhft/affinity/Affinity.java +++ b/affinity/src/main/java/net/openhft/affinity/Affinity.java @@ -39,9 +39,24 @@ public enum Affinity { static final Logger LOGGER = LoggerFactory.getLogger(Affinity.class); @NotNull private static final IAffinity AFFINITY_IMPL; + /** + * Cached reference to {@code Thread.tid} when available, otherwise {@code null}. + */ + private static final Field THREAD_TID_FIELD; private static Boolean JNAAvailable; static { + Field tidField = null; + try { + tidField = Thread.class.getDeclaredField("tid"); + tidField.setAccessible(true); + } catch (NoSuchFieldException e) { + LOGGER.info("Thread.tid field not present; thread id won't be set via reflection"); + } catch (Exception e) { + LOGGER.warn("Unable to access Thread.tid field", e); + } + THREAD_TID_FIELD = tidField; + String osName = System.getProperty("os.name"); if (osName.contains("Win") && isWindowsJNAAffinityUsable()) { LOGGER.trace("Using Windows JNA-based affinity control implementation"); @@ -175,12 +190,14 @@ public static int getThreadId() { } public static void setThreadId() { + int threadId = Affinity.getThreadId(); + if (THREAD_TID_FIELD == null) { + LOGGER.info("Thread.tid field unavailable; skipping setting thread id {}", threadId); + return; + } try { - int threadId = Affinity.getThreadId(); - final Field tid = Thread.class.getDeclaredField("tid"); - tid.setAccessible(true); final Thread thread = Thread.currentThread(); - tid.setLong(thread, threadId); + THREAD_TID_FIELD.setLong(thread, threadId); Affinity.LOGGER.info("Set {} to thread id {}", thread.getName(), threadId); } catch (Exception e) { throw new IllegalStateException(e); @@ -190,11 +207,10 @@ public static void setThreadId() { public static boolean isJNAAvailable() { if (JNAAvailable == null) { int majorVersion = Integer.parseInt(Native.VERSION.split("\\.")[0]); - if(majorVersion < 5) { + if (majorVersion < 5) { LOGGER.warn("Affinity library requires JNA version >= 5"); JNAAvailable = false; - } - else { + } else { try { Class.forName("com.sun.jna.Platform"); JNAAvailable = true; diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityLock.java b/affinity/src/main/java/net/openhft/affinity/AffinityLock.java index 5954efcb4..dfa3f6dc5 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityLock.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityLock.java @@ -69,6 +69,7 @@ public class AffinityLock implements Closeable { * 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. */ @@ -88,9 +89,10 @@ public class AffinityLock implements Closeable { 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; } @@ -131,9 +133,9 @@ private static BitSet getReservedAffinity0() { reservedAffinity = reservedAffinity.trim(); long[] longs = new long[1 + (reservedAffinity.length() - 1) / 16]; int end = reservedAffinity.length(); - for(int i = 0; i < longs.length ; i++) { + for (int i = 0; i < longs.length; i++) { int begin = Math.max(0, end - 16); - longs[i] = Long.parseLong(reservedAffinity.substring(begin, end), 16); + longs[i] = Long.parseUnsignedLong(reservedAffinity.substring(begin, end), 16); end = begin; } return BitSet.valueOf(longs); @@ -148,14 +150,6 @@ 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. @@ -186,6 +180,9 @@ public static AffinityLock acquireLock(boolean bind) { * @return A handle for an affinity lock. */ public static AffinityLock acquireLock(int cpuId) { + if (cpuId < 0 || cpuId >= PROCESSORS) { + throw new IllegalArgumentException("cpuId must be between 0 and " + (PROCESSORS - 1) + ": " + cpuId); + } return acquireLock(true, cpuId, AffinityStrategies.ANY); } @@ -199,17 +196,18 @@ public static AffinityLock acquireLock(int cpuId) { * @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 ) - { + for (int cpu : cpus) { + if (cpu < 0 || cpu >= PROCESSORS) { + throw new IllegalArgumentException("cpuId must be between 0 and " + (PROCESSORS - 1) + ": " + cpu); + } AffinityLock lock = tryAcquireLock(true, cpu); - if(lock != null) - { + 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)); + LOGGER.warn("Failed to lock any CPU in explicit list {}", Arrays.toString(cpus)); return LOCK_INVENTORY.noLock(); } @@ -228,7 +226,7 @@ public static AffinityLock acquireLockLastMinus(int n) { *

    *
  • "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
  • + *
  • "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
  • @@ -261,7 +259,7 @@ public static AffinityLock acquireLock(String desc) { } else if (desc.startsWith("csv:")) { String content = desc.substring(4); - int[] cpus = Arrays.asList(content.split(",")).stream() + int[] cpus = Arrays.stream(content.split(",")) .map(String::trim) .mapToInt(Integer::parseInt).toArray(); @@ -281,7 +279,7 @@ public static AffinityLock acquireLock(String desc) { } } if (cpuId <= 0) { - System.err.println("Cannot allocate 0 or negative cpuIds '" + desc + "'"); + LOGGER.warn("Cannot allocate 0 or negative cpuIds '{}'", desc); return LOCK_INVENTORY.noLock(); } return acquireLock(cpuId); @@ -309,7 +307,7 @@ private static AffinityLock acquireLock(boolean bind, int cpuId, @NotNull Affini * 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 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 */ @@ -332,7 +330,9 @@ public static String dumpLocks() { private static boolean areAssertionsEnabled() { boolean debug = false; + //noinspection AssertWithSideEffects assert debug = true; + //noinspection ConstantValue return debug; } @@ -450,7 +450,7 @@ public void close() { @Override protected void finalize() throws Throwable { if (bound) { - LOGGER.warn("Affinity lock for " + assignedThread + " was discarded rather than release()d in a controlled manner.", boundHere); + LOGGER.warn("Affinity lock for {} was discarded rather than release()d in a controlled manner.", assignedThread, boundHere); release(); } super.finalize(); @@ -463,6 +463,10 @@ public int cpuId() { return cpuId; } + public int cpuId2() { + return cpuId2; + } + /** * @return Was a cpu found to bind this lock to. */ @@ -490,4 +494,12 @@ else if (base) sb.append("CPU not available"); return sb.toString(); } + + 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)); + } + } + } } diff --git a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java index 91bd4d29b..7f789a4e5 100644 --- a/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java +++ b/affinity/src/main/java/net/openhft/affinity/AffinityThreadFactory.java @@ -53,13 +53,11 @@ 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() { - try (AffinityLock ignored = acquireLockBasedOnLast()) { - assert ignored != null; - r.run(); - } + Thread t = new Thread(() -> { + try (AffinityLock ignored = acquireLockBasedOnLast()) { + //noinspection ConstantValue + assert ignored != null; + r.run(); } }, name2); t.setDaemon(daemon); diff --git a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java index 9dfa07f9d..b13d16684 100644 --- a/affinity/src/main/java/net/openhft/affinity/BootClassPath.java +++ b/affinity/src/main/java/net/openhft/affinity/BootClassPath.java @@ -17,10 +17,13 @@ package net.openhft.affinity; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; +import java.net.URI; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collections; @@ -38,21 +41,56 @@ enum BootClassPath { 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", ""); - logger.trace("Boot class-path is: {}", bootClassPath); + if (!bootClassPath.isEmpty()) { + logger.trace("Boot class-path is: {}", bootClassPath); - final String pathSeparator = System.getProperty("path.separator"); - logger.trace("Path separator is: '{}'", pathSeparator); + final String pathSeparator = File.pathSeparator; + logger.trace("Path separator is: '{}'", pathSeparator); - final String[] pathElements = bootClassPath.split(pathSeparator); + final String[] pathElements = bootClassPath.split(pathSeparator); - for (final String pathElement : pathElements) { - resources.addAll(findResources(Paths.get(pathElement), logger)); + for (final String pathElement : pathElements) { + resources.addAll(findResources(Paths.get(pathElement), logger)); + } + } else { + resources.addAll(findResourcesInJrt(logger)); } return resources; } + private static Set findResourcesInJrt(final Logger logger) { + final Set jrtResources = new HashSet<>(); + try { + 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; + } + private static Set findResources(final Path path, final Logger logger) { if (!Files.exists(path)) { return Collections.emptySet(); @@ -87,7 +125,7 @@ private static Set findResourcesInDirectory(final Path path, final Logge try { Files.walkFileTree(path, new SimpleFileVisitor() { @Override - public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { + 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()); } @@ -95,7 +133,7 @@ public FileVisitResult visitFile(final Path file, final BasicFileAttributes attr } }); } catch (IOException e) { - logger.warn("Error walking dir: " + path, e); + logger.warn("Error walking dir: {}", path, e); } return dirResources; diff --git a/affinity/src/main/java/net/openhft/affinity/CpuLayout.java b/affinity/src/main/java/net/openhft/affinity/CpuLayout.java index 7221d7dfb..12cbce61e 100644 --- a/affinity/src/main/java/net/openhft/affinity/CpuLayout.java +++ b/affinity/src/main/java/net/openhft/affinity/CpuLayout.java @@ -49,4 +49,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/LockCheck.java b/affinity/src/main/java/net/openhft/affinity/LockCheck.java index 259aaf40b..2953637c3 100644 --- a/affinity/src/main/java/net/openhft/affinity/LockCheck.java +++ b/affinity/src/main/java/net/openhft/affinity/LockCheck.java @@ -17,6 +17,7 @@ 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; @@ -39,9 +40,7 @@ public enum LockCheck { private static final LockChecker lockChecker = FileLockBasedLockChecker.getInstance(); public static long getPID() { - String processName = - java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); - return Long.parseLong(processName.split("@")[0]); + return Utilities.currentProcessId(); } static boolean canOSSupportOperation() { @@ -55,8 +54,8 @@ public static boolean isCpuFree(int cpu) { return isLockFree(cpu); } - static boolean replacePid(int cpu, long processID) throws IOException { - return storePid(processID, cpu); + static boolean replacePid(int cpu, int cpu2, long processID) throws IOException { + return storePid(processID, cpu, cpu2); } public static boolean isProcessRunning(long pid) { @@ -70,8 +69,8 @@ public static boolean isProcessRunning(long pid) { * 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) throws IOException { - return lockChecker.obtainLock(cpu, Long.toString(processID)); + 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) { @@ -79,6 +78,9 @@ private synchronized static boolean isLockFree(int id) { } public static int getProcessForCpu(int core) throws IOException { + if (!canOSSupportOperation()) + return EMPTY_PID; + String meta = lockChecker.getMetaInfo(core); if (meta != null && !meta.isEmpty()) { @@ -91,10 +93,10 @@ public static int getProcessForCpu(int core) throws IOException { return EMPTY_PID; } - static boolean updateCpu(int cpu) throws IOException { + static boolean updateCpu(int cpu, int cpu2) throws IOException { if (!canOSSupportOperation()) return true; - return replacePid(cpu, getPID()); + return replacePid(cpu, cpu2, getPID()); } public static void releaseLock(int cpu) { diff --git a/affinity/src/main/java/net/openhft/affinity/LockInventory.java b/affinity/src/main/java/net/openhft/affinity/LockInventory.java index 4b96edeec..0fa41b114 100644 --- a/affinity/src/main/java/net/openhft/affinity/LockInventory.java +++ b/affinity/src/main/java/net/openhft/affinity/LockInventory.java @@ -53,7 +53,7 @@ public static String dumpLocks(@NotNull AffinityLock[] locks) { for (int i = 0; i < locks.length; i++) { AffinityLock al = locks[i]; sb.append(i).append(": "); - sb.append(al.toString()); + sb.append(al); sb.append('\n'); } return sb.toString(); @@ -82,7 +82,7 @@ private static boolean isAnyCpu(final int cpuId) { */ private static boolean updateLockForCurrentThread(final boolean bind, final AffinityLock al, final boolean wholeCore) throws ClosedByInterruptException { try { - if (LockCheck.updateCpu(al.cpuId())) { + if (LockCheck.updateCpu(al.cpuId(), wholeCore ? al.cpuId2() : 0)) { al.assignCurrentThread(bind, wholeCore); return true; } @@ -90,7 +90,7 @@ private static boolean updateLockForCurrentThread(final boolean bind, final Affi throw e; } catch (IOException e) { - LOGGER.info("Error occurred acquiring lock, trying another " + e); + LOGGER.info("Error occurred acquiring lock, trying another {}", String.valueOf(e)); } return false; } @@ -108,7 +108,9 @@ public final synchronized void set(CpuLayout cpuLayout) { final boolean base = AffinityLock.BASE_AFFINITY.get(i); final boolean reservable = AffinityLock.RESERVED_AFFINITY.get(i); LOGGER.trace("cpu {} base={} reservable= {}", i, base, reservable); - AffinityLock lock = logicalCoreLocks[i] = newLock(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); @@ -127,7 +129,7 @@ public final synchronized void set(CpuLayout cpuLayout) { private void shrink(NavigableMap physicalCoreLocks) { for (Map.Entry e : physicalCoreLocks.entrySet()) { final AffinityLock[] locks = e.getValue(); - for (int i=0; i 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 d3e9c89c1..74ff2d580 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/VanillaCpuLayout.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/VanillaCpuLayout.java @@ -23,6 +23,7 @@ import org.slf4j.LoggerFactory; import java.io.*; +import java.nio.charset.StandardCharsets; import java.util.*; import static java.lang.Integer.parseInt; @@ -114,7 +115,7 @@ 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<>(); CpuInfo details = new CpuInfo(); @@ -176,6 +177,19 @@ public int threadId(int cpuId) { return cpuDetails.get(cpuId).threadId; } + @Override + public int pair(int cpuId) { + for (int i = 0; i < cpuDetails.size(); i++) { + CpuInfo info = cpuDetails.get(i); + if (info.socketId == cpuDetails.get(cpuId).socketId && + info.coreId == cpuDetails.get(cpuId).coreId && + info.threadId != cpuDetails.get(cpuId).threadId) { + return i; + } + } + return 0; + } + @NotNull @Override public String toString() { diff --git a/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java b/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java index 5ad224302..616f49232 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/VersionHelper.java @@ -30,7 +30,7 @@ public VersionHelper(int major_, int minor_, int release_) { } public VersionHelper(String ver) { - if (ver != null && (ver = ver.trim()).length() > 0) { + if (ver != null && !(ver = ver.trim()).isEmpty()) { final String[] parts = ver.split("\\."); major = parts.length > 0 ? Integer.parseInt(parts[0]) : 0; minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; @@ -46,7 +46,7 @@ public String toString() { } public boolean equals(Object o) { - if (o != null && (o instanceof VersionHelper)) { + if (o instanceof VersionHelper) { VersionHelper ver = (VersionHelper) o; return this.major == ver.major && this.minor == ver.minor 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 27b3af156..c64bd15be 100644 --- a/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java +++ b/affinity/src/main/java/net/openhft/affinity/impl/WindowsJNAAffinity.java @@ -89,8 +89,9 @@ public void setAffinity(final BitSet affinity) { throw new IllegalStateException("SetThreadAffinityMask((" + pid + ") , &(" + affinity + ") ) errorNo=" + e.getErrorCode(), e); } BitSet affinity2 = getAffinity0(); - if (!affinity2.equals(affinity)) { - LoggerFactory.getLogger(WindowsJNAAffinity.class).warn("Tried to set affinity to " + affinity + " but was " + affinity2 + " you may have insufficient access rights"); + 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()); } diff --git a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java index c13040e1d..265d8923f 100644 --- a/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/FileLockBasedLockChecker.java @@ -72,7 +72,7 @@ public synchronized boolean isLockFree(int id) { 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); + LOGGER.debug("Couldn't delete orphaned lock file {}", lockFile); } return true; } else { @@ -98,21 +98,32 @@ public synchronized boolean isLockFree(int id) { } @Override - public synchronized boolean obtainLock(int id, String metaInfo) throws IOException { + 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) { - locks[id] = lockReference; - return true; + 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 " + id + ", failing acquire"); + LOGGER.warn("Exceeded maximum retries for locking CPU {}, failing acquire", id); return false; } @@ -163,6 +174,7 @@ private void writeMetaInfoToFile(FileChannel fc, String metaInfo) throws IOExcep 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); } } @@ -172,7 +184,7 @@ 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); + LOGGER.warn("Couldn't delete lock file on release: {}", lockFile); } closeQuietly(locks[id].lock, locks[id].channel); locks[id] = null; @@ -188,7 +200,7 @@ private void closeQuietly(AutoCloseable... closeables) { closeable.close(); } } catch (Exception e) { - LOGGER.warn("Error closing " + closeable.getClass().getName(), e); + LOGGER.warn("Error closing {}", closeable.getClass().getName(), e); } } } @@ -238,7 +250,7 @@ private File tmpDir() { /** * Thrown when another process deleted the lock file between us opening the file and acquiring the lock */ - class ConcurrentLockFileDeletionException extends Exception { + 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 index 8df4eafa1..62a6c2f52 100644 --- a/affinity/src/main/java/net/openhft/affinity/lockchecker/LockChecker.java +++ b/affinity/src/main/java/net/openhft/affinity/lockchecker/LockChecker.java @@ -26,7 +26,7 @@ public interface LockChecker { boolean isLockFree(int id); - boolean obtainLock(int id, String metaInfo) throws IOException; + boolean obtainLock(int id, int id2, String metaInfo) throws IOException; boolean releaseLock(int id); diff --git a/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java b/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java index 01ec845b3..78ca56302 100644 --- a/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java +++ b/affinity/src/main/java/net/openhft/affinity/main/AffinityTestMain.java @@ -29,11 +29,11 @@ public class AffinityTestMain { public static void main(String[] args) { - int cpus = 1; + int cpus; if (args.length == 0) { cpus = AffinityLock.cpuLayout().cpus() / 12; } else { - cpus = Integer.valueOf(args[0]); + cpus = Integer.parseInt(args[0]); } for (int i = 0; i < cpus; i++) { @@ -50,9 +50,10 @@ private static void acquireAndDoWork() { 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.toString()); + 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 diff --git a/affinity/src/main/java/net/openhft/ticker/ITicker.java b/affinity/src/main/java/net/openhft/ticker/ITicker.java index 5438e51d8..0763f0871 100644 --- a/affinity/src/main/java/net/openhft/ticker/ITicker.java +++ b/affinity/src/main/java/net/openhft/ticker/ITicker.java @@ -17,8 +17,23 @@ package net.openhft.ticker; -/* - * Created by Peter Lawrey on 13/07/15. +/** + * Abstraction of a high resolution time source used throughout the library. + *

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

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

    + * This interface is typically accessed via the {@link net.openhft.ticker.Ticker} + * helper class which selects the best available implementation for the + * running platform. */ public interface ITicker { long nanoTime(); diff --git a/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java b/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java index b493d6020..58cb01ab6 100644 --- a/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java +++ b/affinity/src/main/java/net/openhft/ticker/impl/JNIClock.java @@ -65,13 +65,14 @@ static long tscToNano(final long tsc) { return (tsc * RDTSC_FACTOR) >> FACTOR_BITS; } + @SuppressWarnings("StatementWithEmptyBody") private static void estimateFrequency(int factor) { final long start = System.nanoTime(); long now; - while ((now = System.nanoTime()) == start) { + while (System.nanoTime() == start) { } - long end = start + factor * 1000000; + long end = start + factor * 1000000L; final long start0 = rdtsc0(); while ((now = System.nanoTime()) < end) { } 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..e981e4e12 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockDumpLocksTest.java @@ -0,0 +1,58 @@ +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 { + + @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(); + } + } + + static void supressUnusedWarning(AutoCloseable c) { + // do nothing + } +} 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..4cbc15f4d --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockReleaseTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity; + +import net.openhft.affinity.impl.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 f4d8bf76e..12bc405ab 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityLockTest.java @@ -19,9 +19,7 @@ import net.openhft.affinity.impl.Utilities; import net.openhft.affinity.impl.VanillaCpuLayout; -import net.openhft.affinity.testimpl.TestFileLockBasedLockChecker; import org.hamcrest.MatcherAssert; -import org.jetbrains.annotations.NotNull; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,9 +27,11 @@ 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.List; +import java.util.BitSet; import static net.openhft.affinity.AffinityLock.PROCESSORS; import static org.hamcrest.CoreMatchers.is; @@ -44,20 +44,18 @@ public class AffinityLockTest extends BaseAffinityTest { private static final Logger logger = LoggerFactory.getLogger(AffinityLockTest.class); - private final TestFileLockBasedLockChecker lockChecker = new TestFileLockBasedLockChecker(); - @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(); @@ -87,10 +85,10 @@ 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(); @@ -110,8 +108,8 @@ 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(); @@ -223,14 +221,11 @@ public void testAffinity() throws InterruptedException { try (AffinityLock al = AffinityLock.acquireLock()) { System.out.println("Main locked"); displayStatus(); - Thread t = new Thread(new Runnable() { - @Override - public void run() { - AffinityLock al2 = al.acquireLock(AffinityStrategies.SAME_SOCKET, AffinityStrategies.ANY); - System.out.println("Thread-0 locked"); - displayStatus(); - al2.release(); - } + Thread t = new Thread(() -> { + AffinityLock al2 = al.acquireLock(AffinityStrategies.SAME_SOCKET, AffinityStrategies.ANY); + System.out.println("Thread-0 locked"); + displayStatus(); + al2.release(); }); t.start(); t.join(); @@ -258,11 +253,31 @@ public void lockFilesShouldBeRemovedOnRelease() { } final AffinityLock lock = AffinityLock.acquireLock(); - assertTrue(Files.exists(Paths.get(lockChecker.doToFile(lock.cpuId()).getAbsolutePath()))); + Path lockFile = Paths.get(System.getProperty("java.io.tmpdir"), "cpu-" + lock.cpuId() + ".lock"); + assertTrue(Files.exists(lockFile)); lock.release(); - assertFalse(Files.exists(Paths.get(lockChecker.doToFile(lock.cpuId()).getAbsolutePath()))); + assertFalse(Files.exists(lockFile)); + } + + @Test + public void wholeCoreLockReservesAllLogicalCpus() throws IOException { + if (!Utilities.ISLINUX || !new File("/proc/cpuinfo").exists()) { + return; + } + AffinityLock.cpuLayout(VanillaCpuLayout.fromCpuInfo()); + + try (AffinityLock lock = AffinityLock.acquireCore()) { + CpuLayout layout = AffinityLock.cpuLayout(); + 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)); + } + } + } } private void displayStatus() { @@ -305,16 +320,54 @@ public void testAffinityLockDescriptions() { } @Test - public void testTooHighCpuId() { - try (AffinityLock ignored = AffinityLock.acquireLock(123456)) { - assertNotNull(ignored); + 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 + @Test(expected = IllegalArgumentException.class) + public void testTooHighCpuId() { + AffinityLock.acquireLock(123456); + } + + @Test(expected = IllegalArgumentException.class) + public void testNegativeCpuId() { + AffinityLock.acquireLock(-1); + } + + @Test(expected = IllegalArgumentException.class) public void testTooHighCpuId2() { - try (AffinityLock ignored = AffinityLock.acquireLock(new int[] {123456})) { - assertNotNull(ignored); + AffinityLock.acquireLock(new int[]{123456}); + } + + @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(); + + while (!lock.isBound()) { + Thread.sleep(10); + } + + 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..502cb0e0b --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityResetToBaseAffinityTest.java @@ -0,0 +1,33 @@ +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/AffinityThreadFactoryMain.java b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java index 9b7960e01..9052239b3 100644 --- a/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java +++ b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryMain.java @@ -37,16 +37,14 @@ private AffinityThreadFactoryMain() { 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..7377cc7ea --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/AffinityThreadFactoryTest.java @@ -0,0 +1,55 @@ +package net.openhft.affinity; + +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +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/BootClassPathTest.java b/affinity/src/test/java/net/openhft/affinity/BootClassPathTest.java index bf0cb8268..9e1db8097 100644 --- a/affinity/src/test/java/net/openhft/affinity/BootClassPathTest.java +++ b/affinity/src/test/java/net/openhft/affinity/BootClassPathTest.java @@ -23,8 +23,6 @@ public class BootClassPathTest { @Test public void shouldDetectClassesOnClassPath() { - if (!System.getProperty("java.version").startsWith("1.8")) - return; 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 index 82b64435a..6be18c5d7 100644 --- a/affinity/src/test/java/net/openhft/affinity/FileLockLockCheckTest.java +++ b/affinity/src/test/java/net/openhft/affinity/FileLockLockCheckTest.java @@ -45,7 +45,7 @@ public void before() { @Test public void test() throws IOException { Assert.assertTrue(LockCheck.isCpuFree(cpu)); - LockCheck.updateCpu(cpu); + LockCheck.updateCpu(cpu, 0); Assert.assertEquals(LockCheck.getPID(), LockCheck.getProcessForCpu(cpu)); } @@ -58,17 +58,50 @@ public void testPidOnLinux() { public void testReplace() throws IOException { cpu++; Assert.assertTrue(LockCheck.isCpuFree(cpu + 1)); - LockCheck.replacePid(cpu, 123L); + LockCheck.replacePid(cpu, 0, 123L); Assert.assertEquals(123L, LockCheck.getProcessForCpu(cpu)); } @Test public void shouldNotBlowUpIfPidFileIsEmpty() throws Exception { - LockCheck.updateCpu(cpu); + 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/LockCheckTest.java b/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java index c7f04465b..2612886ce 100644 --- a/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java +++ b/affinity/src/test/java/net/openhft/affinity/LockCheckTest.java @@ -46,7 +46,7 @@ public void before() { @Test public void test() throws IOException { Assert.assertTrue(LockCheck.isCpuFree(cpu)); - LockCheck.updateCpu(cpu); + LockCheck.updateCpu(cpu, 0); Assert.assertEquals(LockCheck.getPID(), LockCheck.getProcessForCpu(cpu)); } @@ -55,17 +55,22 @@ 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, 123L); + LockCheck.replacePid(cpu, 0, 123L); Assert.assertEquals(123L, LockCheck.getProcessForCpu(cpu)); } @Test public void shouldNotBlowUpIfPidFileIsEmpty() throws Exception { - LockCheck.updateCpu(cpu); + LockCheck.updateCpu(cpu, 0); final File file = lockChecker.doToFile(cpu); new RandomAccessFile(file, "rw").setLength(0); @@ -75,7 +80,7 @@ public void shouldNotBlowUpIfPidFileIsEmpty() throws Exception { @Test public void shouldNotBlowUpIfPidFileIsCorrupt() throws Exception { - LockCheck.updateCpu(cpu); + LockCheck.updateCpu(cpu, 0); final File file = lockChecker.doToFile(cpu); try (final FileWriter writer = new FileWriter(file, false)) { diff --git a/affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java b/affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java index ed96007fb..6aa948297 100644 --- a/affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java +++ b/affinity/src/test/java/net/openhft/affinity/MultiProcessAffinityTest.java @@ -53,7 +53,7 @@ public void setUp() { } @Test - public void shouldNotAcquireLockOnCoresLockedByOtherProcesses() throws IOException, InterruptedException { + public void shouldNotAcquireLockOnCoresLockedByOtherProcesses() throws InterruptedException { // run the separate affinity locker final Process affinityLockerProcess = JavaProcessBuilder.create(AffinityLockerProcess.class) .withJvmArguments("-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()) @@ -64,6 +64,7 @@ public void shouldNotAcquireLockOnCoresLockedByOtherProcesses() throws IOExcepti // 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={}", @@ -83,7 +84,7 @@ public void shouldNotAcquireLockOnCoresLockedByOtherProcesses() throws IOExcepti } @Test - public void shouldAllocateCoresCorrectlyUnderContention() throws IOException, InterruptedException { + 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); @@ -99,7 +100,7 @@ public void shouldAllocateCoresCorrectlyUnderContention() throws IOException, In } @Test - public void shouldAllocateCoresCorrectlyUnderContentionWithFailures() throws IOException, InterruptedException { + 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); @@ -118,7 +119,7 @@ public void shouldAllocateCoresCorrectlyUnderContentionWithFailures() throws IOE } @Test - public void shouldBeAbleToAcquireLockLeftByOtherProcess() throws IOException, InterruptedException { + public void shouldBeAbleToAcquireLockLeftByOtherProcess() throws InterruptedException { final Process process = JavaProcessBuilder.create(AffinityLockerThatDoesNotReleaseProcess.class) .withJvmArguments("-Djava.io.tmpdir=" + folder.getRoot().getAbsolutePath()) .withProgramArguments("last").start(); @@ -214,12 +215,12 @@ public static void main(String[] args) { String cpuIdToLock = args[0]; try (final AffinityLock affinityLock = AffinityLock.acquireLock(cpuIdToLock)) { - LOGGER.info("Got affinity lock " + affinityLock + " at " + LocalDateTime.now() + ", CPU=" + affinityLock.cpuId()); + 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 " + LocalDateTime.now() + " lock is released"); + LOGGER.info("Interrupted at {} lock is released", LocalDateTime.now()); } } } @@ -228,13 +229,13 @@ public static void main(String[] args) { * Acquires a lock then ends */ static class AffinityLockerThatDoesNotReleaseProcess { - private static final Logger LOGGER = LoggerFactory.getLogger(AffinityLockerProcess.class); + 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 " + affinityLock + " at " + LocalDateTime.now() + ", CPU=" + affinityLock.cpuId()); + LOGGER.info("Got affinity lock {} at {}, CPU={}", affinityLock, LocalDateTime.now(), affinityLock.cpuId()); } } @@ -253,12 +254,14 @@ public static void main(String[] args) throws InterruptedException, IOException 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)); } } diff --git a/affinity/src/test/java/net/openhft/affinity/SetThreadIdTest.java b/affinity/src/test/java/net/openhft/affinity/SetThreadIdTest.java new file mode 100644 index 000000000..aa6c1caf1 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/SetThreadIdTest.java @@ -0,0 +1,25 @@ +package net.openhft.affinity; + +import org.junit.Assume; +import org.junit.Test; + +import java.lang.reflect.Field; + +import static org.junit.Assert.assertEquals; + +public class SetThreadIdTest { + + @Test + public void setThreadIdShouldUpdateThreadTidField() throws Exception { + Field tidFieldField = Affinity.class.getDeclaredField("THREAD_TID_FIELD"); + tidFieldField.setAccessible(true); + Field tidField = (Field) tidFieldField.get(null); + Assume.assumeTrue(tidField != null); + tidField.setAccessible(true); + + Affinity.setThreadId(); + long expected = Affinity.getThreadId(); + long actual = tidField.getLong(Thread.currentThread()); + assertEquals(expected, actual); + } +} 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 02ed43383..5757e32ba 100644 --- a/affinity/src/test/java/net/openhft/affinity/impl/AbstractAffinityImplTest.java +++ b/affinity/src/test/java/net/openhft/affinity/impl/AbstractAffinityImplTest.java @@ -23,8 +23,7 @@ import java.util.BitSet; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * @author cheremin @@ -49,10 +48,7 @@ public void getAffinityCompletesGracefully() { @Test public void getAffinityReturnsValidValue() { final BitSet affinity = getImpl().getAffinity(); - assertTrue( - "Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", - !affinity.isEmpty() - ); + assertFalse("Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", affinity.isEmpty()); final long allCoresMask = (1L << CORES) - 1; assertTrue( "Affinity mask " + Utilities.toBinaryString(affinity) + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", 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..3cee5d4bc --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/CpuInfoLayoutMappingTest.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +public class CpuInfoLayoutMappingTest { + + @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/UtilitiesTest.java b/affinity/src/test/java/net/openhft/affinity/impl/UtilitiesTest.java new file mode 100644 index 000000000..45d05dbfd --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/UtilitiesTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.Test; + +import java.util.BitSet; + +import static org.junit.Assert.assertEquals; + +public class UtilitiesTest { + + 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..99986acd3 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPairTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2025 chronicle.software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.openhft.affinity.impl; + +import org.junit.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 { + + @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..b539ccf98 --- /dev/null +++ b/affinity/src/test/java/net/openhft/affinity/impl/VanillaCpuLayoutPropertiesParseTest.java @@ -0,0 +1,50 @@ +package net.openhft.affinity.impl; + +import org.junit.Test; + +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; + +public class VanillaCpuLayoutPropertiesParseTest { + + @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/software/chronicle/enterprise/internals/JnaAffinityTest.java b/affinity/src/test/java/software/chronicle/enterprise/internals/JnaAffinityTest.java index db6fa69f4..85f0d7372 100644 --- a/affinity/src/test/java/software/chronicle/enterprise/internals/JnaAffinityTest.java +++ b/affinity/src/test/java/software/chronicle/enterprise/internals/JnaAffinityTest.java @@ -27,8 +27,7 @@ import java.util.BitSet; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * @author peter.lawrey @@ -54,10 +53,7 @@ public void getAffinityCompletesGracefully() { @Test public void getAffinityReturnsValidValue() { final BitSet affinity = getImpl().getAffinity(); - assertTrue( - "Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", - !affinity.isEmpty() - ); + assertFalse("Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", affinity.isEmpty()); final int allCoresMask = (1 << CORES) - 1; assertTrue( "Affinity mask " + Utilities.toBinaryString(affinity) + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", @@ -80,8 +76,7 @@ public void getAffinityReturnsValuePreviouslySet() { return; } final IAffinity impl = LinuxJNAAffinity.INSTANCE; - final int cores = CORES; - for (int core = 0; core < cores; core++) { + for (int core = 0; core < CORES; core++) { final BitSet mask = new BitSet(); mask.set(core, true); getAffinityReturnsValuePreviouslySet(impl, mask); diff --git a/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java b/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java index 33de98652..6534082b6 100644 --- a/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java +++ b/affinity/src/test/java/software/chronicle/enterprise/internals/NativeAffinityTest.java @@ -25,8 +25,7 @@ import java.util.BitSet; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; /** * @author peter.lawrey @@ -52,10 +51,7 @@ public void getAffinityCompletesGracefully() { @Test public void getAffinityReturnsValidValue() { final BitSet affinity = getImpl().getAffinity(); - assertTrue( - "Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", - !affinity.isEmpty() - ); + assertFalse("Affinity mask " + Utilities.toBinaryString(affinity) + " must be non-empty", affinity.isEmpty()); final int allCoresMask = (1 << CORES) - 1; assertTrue( "Affinity mask " + Utilities.toBinaryString(affinity) + " must be <=(2^" + CORES + "-1 = " + allCoresMask + ")", @@ -79,8 +75,7 @@ public void getAffinityReturnsValuePreviouslySet() { return; } final IAffinity impl = NativeAffinity.INSTANCE; - final int cores = CORES; - for (int core = 0; core < cores; core++) { + for (int core = 0; core < CORES; core++) { final BitSet mask = new BitSet(); mask.set(core, true); getAffinityReturnsValuePreviouslySet(impl, mask); diff --git a/affinity/src/test/resources/amd64.dual.core.properties b/affinity/src/test/resources/amd64.dual.core.properties new file mode 100644 index 000000000..3f74c06bc --- /dev/null +++ b/affinity/src/test/resources/amd64.dual.core.properties @@ -0,0 +1,18 @@ +# +# 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 diff --git a/affinity/src/test/resources/amd64.quad.core.properties b/affinity/src/test/resources/amd64.quad.core.properties new file mode 100644 index 000000000..3f681ff10 --- /dev/null +++ b/affinity/src/test/resources/amd64.quad.core.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,1,0 +2=0,2,0 +3=0,3,0 diff --git a/affinity/src/test/resources/core.duo.properties b/affinity/src/test/resources/core.duo.properties new file mode 100644 index 000000000..3f74c06bc --- /dev/null +++ b/affinity/src/test/resources/core.duo.properties @@ -0,0 +1,18 @@ +# +# 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 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/q6600.noht.properties b/affinity/src/test/resources/q6600.noht.properties new file mode 100644 index 000000000..e2b34e4e1 --- /dev/null +++ b/affinity/src/test/resources/q6600.noht.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,1,0 +3=0,3,0 diff --git a/affinity/src/test/resources/q6600.vm.properties b/affinity/src/test/resources/q6600.vm.properties new file mode 100644 index 000000000..3f681ff10 --- /dev/null +++ b/affinity/src/test/resources/q6600.vm.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,1,0 +2=0,2,0 +3=0,3,0