From 2f1277e239c929c1d28485681201d211e7bcf2f1 Mon Sep 17 00:00:00 2001 From: Denis Lochmelis <30842273+DLochmelis33@users.noreply.github.com> Date: Wed, 26 Jun 2024 15:14:29 +0200 Subject: [PATCH] Update main branch (#13) --------- Signed-off-by: Evgeniy Moiseenko Co-authored-by: Evgeniy Moiseenko --- .gitignore | 3 +- README.md | 166 ++++---- build.gradle.kts | 4 +- cli/build.gradle.kts | 47 +++ .../org/jetbrains/litmuskt/CliCommon.kt | 125 ++++++ cli/src/jvmMain/kotlin/JvmMain.kt | 3 + .../kotlin/org/jetbrains/litmuskt/CliJvm.kt | 86 ++++ cli/src/nativeMain/kotlin/NativeMain.kt | 4 + .../org/jetbrains/litmuskt}/CliNative.kt | 14 +- codegen/build.gradle.kts | 19 +- .../komem/litmus/LitmusTestProcessor.kt | 58 --- .../jetbrains/litmuskt/LitmusTestProcessor.kt | 94 +++++ ...ols.ksp.processing.SymbolProcessorProvider | 2 +- core/build.gradle.kts | 50 +++ .../org/jetbrains/litmuskt}/AffinityMap.kt | 2 +- .../kotlin/org/jetbrains/litmuskt}/Barrier.kt | 2 +- .../jetbrains/litmuskt}/LitmusOutcomeStats.kt | 39 +- .../jetbrains/litmuskt}/LitmusRunParams.kt | 4 +- .../org/jetbrains/litmuskt/LitmusRunner.kt | 138 +++++++ .../org/jetbrains/litmuskt}/LitmusTest.kt | 22 +- .../kotlin/org/jetbrains/litmuskt}/Utils.kt | 5 +- .../autooutcomes/BooleanAutoOutcomes.kt | 28 ++ .../litmuskt/autooutcomes/IntAutoOutcomes.kt | 97 +++++ .../autooutcomes/LitmusAutoOutcome.kt | 29 ++ .../litmuskt/autooutcomes/LongAutoOutcomes.kt | 97 +++++ .../jetbrains/litmuskt}/LitmusOutcomeTest.kt | 3 + .../org/jetbrains/litmuskt/JvmThreadRunner.kt | 36 ++ .../org/jetbrains/litmuskt}/JvmUtils.kt | 2 +- .../litmuskt}/barriers/JvmCyclicBarrier.kt | 3 +- .../litmuskt}/barriers/JvmSpinBarrier.kt | 3 +- .../litmuskt/AffinityBindingsImplPosix.kt | 42 ++ .../litmuskt/AffinityBindingsImplNoop.kt | 5 + .../src/nativeInterop/barrier.def | 0 {litmus => core}/src/nativeInterop/barrier.h | 0 .../src/nativeInterop/kaffinity.def | 6 +- core/src/nativeInterop/kpthread.def | 36 ++ .../jetbrains/litmuskt}/AffinityManager.kt | 9 +- .../org/jetbrains/litmuskt/NativeUtils.kt | 18 + .../org/jetbrains/litmuskt/PthreadRunner.kt | 77 ++++ .../org/jetbrains/litmuskt}/WorkerRunner.kt | 49 ++- .../litmuskt}/barriers/CinteropSpinBarrier.kt | 3 +- .../litmuskt}/barriers/KNativeSpinBarrier.kt | 3 +- gradle.properties | 7 + gradle/wrapper/gradle-wrapper.properties | 2 +- {jcstests => jcstress-tests}/Custom_LB.java | 0 jcstress-wrapper/build.gradle.kts | 42 ++ jcstress-wrapper/src/main/kotlin/Main.kt | 26 ++ .../kotlin/org/jetbrains/litmuskt/Codegen.kt | 149 +++++++ .../org/jetbrains/litmuskt/JCStressRunner.kt | 163 ++++++++ jcstress/.gitignore | 6 + jcstress/pom.xml | 175 ++++++++ litmus/build.gradle.kts | 155 ------- .../kotlin/komem/litmus/CliCommon.kt | 118 ------ .../kotlin/komem/litmus/LitmusAutoOutcome.kt | 29 -- .../kotlin/komem/litmus/LitmusRunner.kt | 85 ---- .../komem/litmus/testsuite/ClassicTests.kt | 382 ------------------ .../komem/litmus/testsuite/CustomTests.kt | 26 -- .../komem/litmus/testsuite/UPUBExtraTests.kt | 101 ----- .../kotlin/komem.litmus/infra/TestDefaults.kt | 21 - .../komem.litmus/testsuite/ClassicTests.kt | 52 --- .../komem.litmus/testsuite/CustomTests.kt | 9 - .../komem.litmus/testsuite/UPUBExtraTests.kt | 18 - litmus/src/jvmMain/kotlin/JvmMain.kt | 4 - .../src/jvmMain/kotlin/komem/litmus/CliJvm.kt | 9 - .../kotlin/komem/litmus/JvmThreadRunner.kt | 29 -- .../komem/litmus/infra/TestDefaults.jvm.kt | 14 - .../komem/litmus/AffinityBindingsImplPosix.kt | 47 --- .../komem/litmus/AffinityBindingsImplNoop.kt | 3 - litmus/src/nativeInterop/kaffinity.def | 1 + litmus/src/nativeInterop/kaffinity.h | 9 - litmus/src/nativeInterop/kaffinity_gnu.o | Bin 0 -> 2248 bytes litmus/src/nativeInterop/setup.sh | 10 - litmus/src/nativeMain/kotlin/NativeMain.kt | 4 - .../kotlin/komem.litmus/NativeUtils.kt | 6 - .../kotlin/komem/litmus/NativeTest.kt | 15 - .../komem/litmus/infra/TestDefaults.native.kt | 14 - settings.gradle.kts | 5 +- testsuite/build.gradle.kts | 34 ++ .../jetbrains/litmuskt/LitmusTestContainer.kt | 4 + .../litmuskt/LitmusTestExtensions.kt | 14 + .../litmuskt/generated/LitmusTestRegistry.kt | 10 + .../jetbrains/litmuskt/tests/ArrayVolatile.kt | 36 ++ .../org/jetbrains/litmuskt/tests/Atomicity.kt | 66 +++ .../org/jetbrains/litmuskt/tests/Coherence.kt | 58 +++ .../IndependentReadsOfIndependentWrites.kt | 70 ++++ .../jetbrains/litmuskt/tests/LoadBuffering.kt | 98 +++++ .../litmuskt/tests/MessagePassing.kt | 101 +++++ .../litmuskt/tests/StoreBuffering.kt | 62 +++ .../litmuskt/tests/UnsafePublication.kt | 134 ++++++ .../jetbrains/litmuskt/tests/WordTearing.kt | 57 +++ .../litmuskt/tests/WordTearingNative.kt | 35 ++ 91 files changed, 2558 insertions(+), 1390 deletions(-) create mode 100644 cli/build.gradle.kts create mode 100644 cli/src/commonMain/kotlin/org/jetbrains/litmuskt/CliCommon.kt create mode 100644 cli/src/jvmMain/kotlin/JvmMain.kt create mode 100644 cli/src/jvmMain/kotlin/org/jetbrains/litmuskt/CliJvm.kt create mode 100755 cli/src/nativeMain/kotlin/NativeMain.kt rename {litmus/src/nativeMain/kotlin/komem.litmus => cli/src/nativeMain/kotlin/org/jetbrains/litmuskt}/CliNative.kt (58%) delete mode 100644 codegen/src/main/kotlin/komem/litmus/LitmusTestProcessor.kt create mode 100644 codegen/src/main/kotlin/org/jetbrains/litmuskt/LitmusTestProcessor.kt create mode 100644 core/build.gradle.kts rename {litmus/src/commonMain/kotlin/komem/litmus => core/src/commonMain/kotlin/org/jetbrains/litmuskt}/AffinityMap.kt (72%) rename {litmus/src/commonMain/kotlin/komem/litmus/barriers => core/src/commonMain/kotlin/org/jetbrains/litmuskt}/Barrier.kt (73%) rename {litmus/src/commonMain/kotlin/komem/litmus => core/src/commonMain/kotlin/org/jetbrains/litmuskt}/LitmusOutcomeStats.kt (71%) rename {litmus/src/commonMain/kotlin/komem/litmus => core/src/commonMain/kotlin/org/jetbrains/litmuskt}/LitmusRunParams.kt (93%) create mode 100644 core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunner.kt rename {litmus/src/commonMain/kotlin/komem/litmus => core/src/commonMain/kotlin/org/jetbrains/litmuskt}/LitmusTest.kt (68%) rename {litmus/src/commonMain/kotlin/komem/litmus => core/src/commonMain/kotlin/org/jetbrains/litmuskt}/Utils.kt (85%) create mode 100644 core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/BooleanAutoOutcomes.kt create mode 100644 core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/IntAutoOutcomes.kt create mode 100644 core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LitmusAutoOutcome.kt create mode 100644 core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LongAutoOutcomes.kt rename {litmus/src/commonTest/kotlin/komem.litmus => core/src/commonTest/kotlin/org/jetbrains/litmuskt}/LitmusOutcomeTest.kt (88%) create mode 100644 core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmThreadRunner.kt rename {litmus/src/jvmMain/kotlin/komem/litmus => core/src/jvmMain/kotlin/org/jetbrains/litmuskt}/JvmUtils.kt (68%) rename {litmus/src/jvmMain/kotlin/komem/litmus => core/src/jvmMain/kotlin/org/jetbrains/litmuskt}/barriers/JvmCyclicBarrier.kt (72%) rename {litmus/src/jvmMain/kotlin/komem/litmus => core/src/jvmMain/kotlin/org/jetbrains/litmuskt}/barriers/JvmSpinBarrier.kt (87%) create mode 100644 core/src/linuxMain/kotlin/org/jetbrains/litmuskt/AffinityBindingsImplPosix.kt create mode 100644 core/src/macosMain/kotlin/org/jetbrains/litmuskt/AffinityBindingsImplNoop.kt rename {litmus => core}/src/nativeInterop/barrier.def (100%) rename {litmus => core}/src/nativeInterop/barrier.h (100%) rename litmus/src/nativeInterop/kaffinity_gnu.c => core/src/nativeInterop/kaffinity.def (86%) create mode 100644 core/src/nativeInterop/kpthread.def rename {litmus/src/nativeMain/kotlin/komem.litmus => core/src/nativeMain/kotlin/org/jetbrains/litmuskt}/AffinityManager.kt (86%) create mode 100644 core/src/nativeMain/kotlin/org/jetbrains/litmuskt/NativeUtils.kt create mode 100644 core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadRunner.kt rename {litmus/src/nativeMain/kotlin/komem.litmus => core/src/nativeMain/kotlin/org/jetbrains/litmuskt}/WorkerRunner.kt (52%) rename {litmus/src/nativeMain/kotlin/komem.litmus => core/src/nativeMain/kotlin/org/jetbrains/litmuskt}/barriers/CinteropSpinBarrier.kt (84%) rename {litmus/src/nativeMain/kotlin/komem.litmus => core/src/nativeMain/kotlin/org/jetbrains/litmuskt}/barriers/KNativeSpinBarrier.kt (87%) rename {jcstests => jcstress-tests}/Custom_LB.java (100%) create mode 100644 jcstress-wrapper/build.gradle.kts create mode 100644 jcstress-wrapper/src/main/kotlin/Main.kt create mode 100644 jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/Codegen.kt create mode 100644 jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/JCStressRunner.kt create mode 100644 jcstress/.gitignore create mode 100644 jcstress/pom.xml delete mode 100644 litmus/build.gradle.kts delete mode 100644 litmus/src/commonMain/kotlin/komem/litmus/CliCommon.kt delete mode 100644 litmus/src/commonMain/kotlin/komem/litmus/LitmusAutoOutcome.kt delete mode 100644 litmus/src/commonMain/kotlin/komem/litmus/LitmusRunner.kt delete mode 100644 litmus/src/commonMain/kotlin/komem/litmus/testsuite/ClassicTests.kt delete mode 100644 litmus/src/commonMain/kotlin/komem/litmus/testsuite/CustomTests.kt delete mode 100644 litmus/src/commonMain/kotlin/komem/litmus/testsuite/UPUBExtraTests.kt delete mode 100644 litmus/src/commonTest/kotlin/komem.litmus/infra/TestDefaults.kt delete mode 100644 litmus/src/commonTest/kotlin/komem.litmus/testsuite/ClassicTests.kt delete mode 100644 litmus/src/commonTest/kotlin/komem.litmus/testsuite/CustomTests.kt delete mode 100644 litmus/src/commonTest/kotlin/komem.litmus/testsuite/UPUBExtraTests.kt delete mode 100644 litmus/src/jvmMain/kotlin/JvmMain.kt delete mode 100644 litmus/src/jvmMain/kotlin/komem/litmus/CliJvm.kt delete mode 100644 litmus/src/jvmMain/kotlin/komem/litmus/JvmThreadRunner.kt delete mode 100644 litmus/src/jvmTest/kotlin/komem/litmus/infra/TestDefaults.jvm.kt delete mode 100644 litmus/src/linuxMain/kotlin/komem/litmus/AffinityBindingsImplPosix.kt delete mode 100644 litmus/src/macosMain/kotlin/komem/litmus/AffinityBindingsImplNoop.kt create mode 100644 litmus/src/nativeInterop/kaffinity.def delete mode 100644 litmus/src/nativeInterop/kaffinity.h create mode 100644 litmus/src/nativeInterop/kaffinity_gnu.o delete mode 100755 litmus/src/nativeInterop/setup.sh delete mode 100755 litmus/src/nativeMain/kotlin/NativeMain.kt delete mode 100644 litmus/src/nativeMain/kotlin/komem.litmus/NativeUtils.kt delete mode 100644 litmus/src/nativeTest/kotlin/komem/litmus/NativeTest.kt delete mode 100644 litmus/src/nativeTest/kotlin/komem/litmus/infra/TestDefaults.native.kt create mode 100644 testsuite/build.gradle.kts create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestContainer.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestExtensions.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/generated/LitmusTestRegistry.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/ArrayVolatile.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Atomicity.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Coherence.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/IndependentReadsOfIndependentWrites.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/LoadBuffering.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/MessagePassing.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/StoreBuffering.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublication.kt create mode 100644 testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/WordTearing.kt create mode 100644 testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/WordTearingNative.kt diff --git a/.gitignore b/.gitignore index b741e1c..5eaa214 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ build/ temp/ *.hprof gitignored/ -litmus/src/nativeInterop/kaffinity.def -litmus/src/nativeInterop/kaffinity_gnu.o +local.properties diff --git a/README.md b/README.md index d3bb0e8..70d234c 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,66 @@ -# LitmusKt +# LitmusKt **LitmusKt** is a litmus testing tool for Kotlin. -Litmus tests are small concurrent programs exposing various relaxed behaviors, -arising due to compiler or hardware optimizations (for example, instruction reordering). +Litmus tests are small concurrent programs exposing various relaxed behaviors, arising due to compiler or hardware +optimizations (for example, instruction reordering). -This project is in an **experimental** stage of the development. +This project is in an **experimental** stage of the development. The tool's API is unstable and might be a subject to a further change. ## Setup Simply clone the project and run `./gradlew build`. -### Running +Note that for Kotlin/JVM this project relies on [jcstress](https://github.com/openjdk/jcstress). -All classic Gradle building tasks (like `run`, `build`, `-debug-` or `-release-`) work as expected. -The only important distinction is that on various platforms these tasks are named differently. +## Running -```bash -# Linux: -./gradlew runReleaseExecutableLinuxX64 +The entry point is the CLI tool residing in `:cli` subproject. You can use the `--help` flag to find the details about +the CLI, but most basic example requires two settings: + +1. Choose a runner with `-r` option +2. After the options are specified, choose the tests to run using regex patterns + +### Running on Native -# MacOS (x86): -./gradlew runReleaseExecutableMacosX64 -# MacOS (arm): -./gradlew runReleaseExecutableMacosArm64 -Parm # don't forget the -Parm flag! +Create an executable and run it: + +```bash +./gradlew :cli:linkReleaseExecutableLinuxX64 +./build/bin/linuxX64/releaseExecutable/cli.kexe -r pthread 'StoreBuffering.*' ``` -Substituting `Release` with `Debug` disables compiler `-opt` flag. +Depending on what you need, you can: -Also, it is possible to build the project manually with Kotlin CLI compiler. -You'd have to either declare several opt-ins or edit the code to remove `expect/actual` and C interop parts. -There aren't many benefits to manual compilation, but it allows at least some way to read the program's LLVM IR bitcode -(using `-Xtemporary-files-dir` compiler flag and then converting the `.bc` file into readable text with `llvm-dis`). +* Switch between `debug` and `release` (which, among other things, toggles the `-opt` compiler flag) +* Specify the platform (`linuxX64` / `macosX64` / `macosArm64`) + +### Running on JVM + +Simply run the project with Gradle: + +```bash +./gradlew :cli:jvmRun --args="-r jcstress -j '-m sanity' 'StoreBuffering.*'" +``` ## Overview A single litmus test consists of the following parts: + * a state shared between threads; * code for each thread; * an outcome — a certain value which is the result of running the test; * a specification listing accepted and forbidden outcomes -The tool runs litmus tests with various parameters, -using the standard techniques also employed by other tools, +The tool runs litmus tests with various parameters, +using the standard techniques also employed by other tools, like [herdtools/litmus7](https://github.com/herd/herdtools7) and [JCStress](https://github.com/openjdk/jcstress). The tool allocates a batch of shared state instances -and runs the threads on one state instance after another, -occasionally synchronizing threads with barriers. -After all threads finish running, states are converted into outcomes, and the same outcomes are counted. -The end result is the list of all different observed outcomes, +and runs the threads on one state instance after another, +occasionally synchronizing threads with barriers. +After all threads finish running, states are converted into outcomes, and the same outcomes are counted. +The end result is the list of all different observed outcomes, their frequencies and their types (accepted, interesting or forbidden). ### Litmus Test Syntax @@ -74,13 +85,11 @@ val StoreBuffering = litmusTest(::StoreBufferingState) { r2 = x } outcome { - listOf(r1, r2) + r1 to r2 } spec { - accept(listOf(0, 1)) - accept(listOf(1, 0)) - accept(listOf(1, 1)) - interesting(listOf(0, 0)) + accept(listOf(0 to 1, 1 to 0, 1 to 1)) + interesting(listOf(0 to 0)) } } ``` @@ -99,24 +108,23 @@ And here is an example of the tool's output: Let us describe the litmus test's declaration. * As a first argument `litmusTest` takes a function producing the shared state instance. -* The second argument is DSL builder lambda, setting up the litmus test. -* `thread { ... }` lambdas set up the code run in different threads of the litmus tests — - these lambdas take shared state instance as a receiver. +* The second argument is DSL builder lambda, setting up the litmus test. +* `thread { ... }` lambdas set up the code run in different threads of the litmus tests — + these lambdas take shared state instance as a receiver. * `outcome { ... }` lambda sets up the outcome of a test obtained after all threads have run — these lambdas also take shared state instance as a receiver. -* the `spec { ... }` lambda classifies the outcomes into acceptable, interesting, and forbidden categories. +* the `spec { ... }` lambda classifies the outcomes into acceptable, interesting, and forbidden categories. Here are a few additional convenient features. * Classes implementing `LitmusAutoOutcome` interface set up an outcome automatically. - There are a few predefined subclasses of this interface. - For example, the class `LitmusIIOutcome` with `II` standing for "int, int" expects two integers as an outcome. - This class have two fields `var r1: Int` and `var r2: Int`. - These fields should be set inside litmus test's threads, and then they will be automatically used to form an outcome `listOf(r1, r2)`. - -* If the outcome is a `List`, you can use a shorter syntax for declaring accepted / interesting / forbidden outcomes. - Just use `accept(vararg outcome)` counterparts to specify expected elements. - + There are a few predefined subclasses of this interface. + For example, the class `LitmusIIOutcome` with `II` standing for "int, int" expects two integers as an outcome. + This class have two fields `var r1: Int` and `var r2: Int`. + These fields should be set inside litmus test's threads, and then they will be automatically used to form an outcome. +* Additionally, if the state implements `LitmusAutoOutcome`, you can use a shorter syntax for declaring accepted / interesting / forbidden outcomes. + For example, for `LitmusIIOutcome` you can use `accept(r1: Int, r2: Int)` to add `(r1, r2)` as an accepted outcome. +* Finally, `LitmusAutoOutcome` is considerably more performant than manually creating any extra outcome object. It is therefore strongly advised to use this interface at all times. * Since each test usually has its own specific state, it is quite useful to use anonymous classes for them. Using these features, the test from above can be shortened as follows: @@ -147,53 +155,61 @@ val StoreBuffering: LitmusTest<*> = litmusTest({ ### Litmus Test Runners -The tests are run with an `LitmusRunner`. -Currently, there are two implementations. -* `WorkerRunner`: runner based on `Worker` API for Kotlin/Native; -* `JvmThreadRunner`: custom threads-based runner for Kotlin/JVM. -* A proper, JCStress based runner for Kotlin/JVM is **in development**. - -`LitmusRunner` has several running functions: - -* `runTest(params, test)` simply runs the test with the given parameters. -* `runTest(duration, params, test)` repeatedly runs the test with the given parameters until the given time duration passes. -* `runTestParallel(instances, ...)` it runs several instances of the test in parallel. -* `runTestParallel(...)` without explicit instances number will run `#{of cpu cores} / #{of threads in test}` instances. +Litmus tests are run with a `LitmusRunner`. This interface has several running functions: -### Entry point +* `runTests(tests, params, timeLimit)` runs several `tests` one after another, each with the given `params`, optionally repeating each test for the duration of `timeLimit`. +* `runSingleTestParallel(test, params, timeLimit = 0, instances = ...)` runs a single test in parallel `instances`, with the given `params` and optionally repeating for `timeLimit`. The default value for `instances` is `#{of cpu cores} / #{of threads in test}`. -Currently, the `main()` functions are the intended way of running particular litmus tests. -A proper CLI interface is in development. +The following implementations of `LitmusRunner` are available: -There is also an option to run the tests with `@Test` annotation using the default parameters. -However, the tests are run in debug mode by the `kotlinx.test` framework. -Running litmus tests in the debug mode can affect their results, potentially hiding some relaxed behaviors. +* For native: + * `WorkerRunner`: based on K/N `Worker` API + * `PthreadRunner`: based on C interop pthread API +* For JVM: + * `JvmThreadRunner`: a simple runner based on Java threads + * `JCStressRunner`: a **special** runner that delegates to JCStress. Note that many of `LitmusRunner` parameters are not applicable to JCStress. Furthermore, there are JCStress-exclusive options as well. ### Litmus Test Parameters -* `AffinityMap`: bindings from thread to CPU cores. - Obtained through `AffinityManager`, which is available from `getAffinityManager()` top-level function. +There is a number of parameters that can be varied between test runs. Their influence on the results can change +drastically depending on the particular test, hardware, and so on. -* `syncEvery`: the number of tests between barrier synchronizations. - Practice shows that on Native the reasonable range is somewhere in the range from 10 to 100, - while on JVM it works best in the range from 1000 to 10000. - This also depends on the particular test. - -* `Barrier`: can be either Kotlin-implemented (`KNativeSpinBarrier`) or C-implemented (`CinteropSpinBarrier`). +* `AffinityMap`: bindings from thread to CPU cores. + Obtained through `AffinityManager`, which is available from `getAffinityManager()` top-level function. +* `syncEvery`: the number of tests between barrier synchronizations. + Practice shows that on Native the reasonable range is somewhere in the range from 10 to 100, + while on JVM it works best in the range from 1000 to 10000. + This highly depends on the particular test. +* `Barrier`: can be either Kotlin-implemented (`KNativeSpinBarrier`) or C-implemented (`CinteropSpinBarrier`). C-implemented might yield better results. On JVM, use `JvmSpinBarrier` in favor of `JvmCyclicBarrier`. Common practice is to iterate through different parameter bundles and aggregate the results across them. -* Function `variateParameters()` takes the cross-product of all passed parameters + +* Function `variateParameters()` takes the cross-product of all passed parameters (hence use `listOf(null)` instead of `emptyList()` for unused arguments). * For results aggregation, use `List.mergeResults()`. -* You can also use `LitmusResult.prettyPrint()` to print the results. +* You can also use `LitmusResult.generateTable()` to format the results into a human-readable table. + +### Project structure + +The project consists of several subprojects: + +* `:core` contains the core infrastructure such as `LitmusTest` and `LitmusRunner` interfaces, etc. +* `:testsuite` contains the litmus tests themselves. +* `:codegen` uses KSP to collect all tests from `:testsuite`. +* `:jcstress-wrapper` contains the code to convert `LitmusTest`-s into JCStress-compatible Java wrappers. +* `:cli` is a user-friendly entry point. -### Notes +## Notes +* If you decide to add some litmus tests, and you wish for them to be registered in the CLI, you must put them into `:testsuite` subproject. Use the existing tests as reference for the proper test structure. * Setting thread affinity is not supported on macOS yet. As such, `getAffinityManager()` returns `null` on macOS. -* For some reason, running a lot of different tests in one go will drastically reduce the performance and weak outcomes' frequency. - For now, please try to avoid running tests for longer than 5 minutes. +* It is possible to run the tests with `@Test` annotation. However, the tests are run in debug mode by + the `kotlinx.test` framework. Running litmus tests in the debug mode can affect their results, potentially hiding some + relaxed behaviors. * In practice, all cases of currently found relaxed behaviors can be consistently found in under a minute of running. -* Avoid creating unnecessary objects inside threads, especially if they get shared. This not only significantly slows down the performance, but can also introduce unexpected relaxed behaviors. -* The tool currently doesn't address the false sharing problem. The memory shuffling API is in development. +* Avoid creating unnecessary objects inside threads, especially if they get shared. This not only significantly slows + down the performance, but can also introduce unexpected relaxed behaviors. +* The tool currently doesn't address the false sharing problem. It has been shown to be fairly significant, but we looked for a solution and found none good enough. This problem can be resolved with a `@Contended`-like annotation in Kotlin, which does not yet exist. +* When writing tests with `LitmusAutoOutcome`, it is possible to achieve a post-processing step similar to JCStress `@Arbiter`. To do that, you can write your code in the `outcome{}` section, and then return `this` from it. An example can be found in the [WordTearing](testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/WordTearing.kt) test. diff --git a/build.gradle.kts b/build.gradle.kts index 85d5923..38ca55b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") version "1.9.10" apply false + kotlin("multiplatform") version "1.9.20" apply false } repositories { @@ -8,6 +8,8 @@ repositories { } subprojects { + group = "org.jetbrains.litmuskt" + version = "1.0-SNAPSHOT" repositories { mavenCentral() gradlePluginPortal() diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts new file mode 100644 index 0000000..74e957f --- /dev/null +++ b/cli/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + kotlin("multiplatform") +} + +kotlin { + val nativeTargets = listOf( + linuxX64(), + // 1) no machine currently available 2) CLI library does not support +// linuxArm64(), + macosX64(), + macosArm64(), + ) + nativeTargets.forEach { target -> + target.binaries { + executable { + entryPoint = "main" + } + } + } + jvm { + withJava() + } + + sourceSets { + commonMain { + val cliktVersion = project.findProperty("cliktVersion") + dependencies { + implementation(project(":core")) + implementation(project(":testsuite")) + implementation("com.github.ajalt.clikt:clikt:$cliktVersion") + } + } + jvmMain { + dependencies { + implementation(project(":jcstress-wrapper")) + } + } + } +} + +tasks.whenTaskAdded { + if (name == "jvmRun") { + dependsOn(":jcstress-wrapper:copyCoreToJCStress") + dependsOn(":jcstress-wrapper:copyTestsuiteToJCStress") + dependsOn(":jcstress-wrapper:run") + } +} diff --git a/cli/src/commonMain/kotlin/org/jetbrains/litmuskt/CliCommon.kt b/cli/src/commonMain/kotlin/org/jetbrains/litmuskt/CliCommon.kt new file mode 100644 index 0000000..7fd03d4 --- /dev/null +++ b/cli/src/commonMain/kotlin/org/jetbrains/litmuskt/CliCommon.kt @@ -0,0 +1,125 @@ +package org.jetbrains.litmuskt + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.arguments.check +import com.github.ajalt.clikt.parameters.arguments.multiple +import com.github.ajalt.clikt.parameters.arguments.transformAll +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.int +import org.jetbrains.litmuskt.generated.LitmusTestRegistry +import kotlin.time.Duration + +abstract class CliCommon : CliktCommand( + name = "litmuskt", + printHelpOnEmptyArgs = true, +) { + companion object { + const val DEFAULT_BATCH_SIZE = 1_000_000 + const val DEFAULT_SYNC_EVERY = 100 + } + + protected open val batchSizeSchedule by option("-b", "--batchSize") + .int().varargValues().default(listOf(DEFAULT_BATCH_SIZE)) + + protected open val syncEverySchedule by option("-s", "--syncEvery") + .int().varargValues().default(listOf(DEFAULT_SYNC_EVERY)) + + protected open val tests by argument("tests") + .multiple(required = true) + .transformAll { args -> + val regexes = args.map { + try { + Regex(it) + } catch (_: IllegalArgumentException) { + fail("invalid regex: $it") + } + } + regexes.flatMap { LitmusTestRegistry[it] } + } + .check("no tests were selected") { it.isNotEmpty() || listOnly } + + protected val PARALLELISM_DISABLED = Int.MAX_VALUE - 1 + protected val PARALLELISM_AUTO = Int.MAX_VALUE - 2 + protected open val parallelism by option("-p", "--parallelism") + .int().optionalValue(PARALLELISM_AUTO).default(PARALLELISM_DISABLED) + .check("value must be in range 2..1000") { + it in 2..1000 || it == PARALLELISM_DISABLED || it == PARALLELISM_AUTO + } + + protected open val duration by option("-d", "--duration") + .convert { Duration.parse(it) } + .default(Duration.ZERO) + .check("value must not be negative") { !it.isNegative() } + + protected abstract val affinityMapSchedule: List + protected abstract val runner: LitmusRunner + protected abstract val barrierProducer: BarrierProducer + // TODO: we don't talk about memshuffler for now + + protected val listOnly by option("-l", "--listOnly").flag() + // TODO: dry run = simply list tests + + override fun run() { + if (listOnly) { + runListOnly() + return + } + echo("selected ${tests.size} tests: \n" + tests.joinToString("\n") { " - " + it.alias }) + echo() + + val paramsList = variateRunParams( + batchSizeSchedule = batchSizeSchedule, + affinityMapSchedule = affinityMapSchedule, + syncPeriodSchedule = syncEverySchedule, + barrierSchedule = listOf(barrierProducer), + ).toList() + if (paramsList.isEmpty()) { + echo("parameters list is empty; ensure no empty lists are used", err = true) + return + } + echo("parameter combinations per each test: ${paramsList.size}") + echo() + + for (test in tests) { + echo("running test ${test.alias}...") + // TODO: handle exceptions + // TODO: print ETA (later: calculate based on part of run) + paramsList.map { params -> + runTest(params, test) + }.mergeResults().let { + echo(it.generateTable()) + } + echo() + } + } + + private fun runTest(params: LitmusRunParams, test: LitmusTest<*>): LitmusResult { + return when (parallelism) { + PARALLELISM_DISABLED -> { + // note: not running all tests here because of changing params + runner.runTests(listOf(test), params, duration).first() + } + PARALLELISM_AUTO -> { + runner.runSingleTestParallel(test, params, timeLimit = duration) + } + else -> { + runner.runSingleTestParallel(test, params, timeLimit = duration, instances = parallelism) + } + } + } + + protected fun runListOnly() { + echo("all known tests:\n" + LitmusTestRegistry.all().joinToString("\n") { " * " + it.alias }) + echo() + echo("selected tests:\n" + tests.joinToString("\n") { " - " + it.alias }) + } +} + +fun commonMain(args: Array, cli: CliCommon) { + try { + cli.main(args) + } catch (e: Exception) { + cli.echo(e.stackTraceToString(), err = true, trailingNewline = true) + } +} diff --git a/cli/src/jvmMain/kotlin/JvmMain.kt b/cli/src/jvmMain/kotlin/JvmMain.kt new file mode 100644 index 0000000..3d9c77a --- /dev/null +++ b/cli/src/jvmMain/kotlin/JvmMain.kt @@ -0,0 +1,3 @@ +import org.jetbrains.litmuskt.CliJvm + +fun main(args: Array) = CliJvm().main(args) diff --git a/cli/src/jvmMain/kotlin/org/jetbrains/litmuskt/CliJvm.kt b/cli/src/jvmMain/kotlin/org/jetbrains/litmuskt/CliJvm.kt new file mode 100644 index 0000000..fd4efce --- /dev/null +++ b/cli/src/jvmMain/kotlin/org/jetbrains/litmuskt/CliJvm.kt @@ -0,0 +1,86 @@ +package org.jetbrains.litmuskt + +import com.github.ajalt.clikt.parameters.groups.OptionGroup +import com.github.ajalt.clikt.parameters.groups.groupChoice +import com.github.ajalt.clikt.parameters.groups.required +import com.github.ajalt.clikt.parameters.options.convert +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import jcstressDirectory +import org.jetbrains.litmuskt.barriers.JvmSpinBarrier + +private sealed class RunnerOptions : OptionGroup() { + abstract val runner: LitmusRunner +} + +private class JvmThreadRunnerOptions : RunnerOptions() { + override val runner = JvmThreadRunner() +} + +private class JCStressRunnerOptions : RunnerOptions() { + private val jcstressFreeArgs by option("-j", "--jcsargs") + .convert { it.split(" ") } + .default(emptyList()) + + override val runner get() = JCStressRunner(jcstressDirectory, jcstressFreeArgs) +} + +class CliJvm : CliCommon() { + private val runnerOptions by option("-r", "--runner").groupChoice( + "thread" to JvmThreadRunnerOptions(), + "jcstress" to JCStressRunnerOptions(), + ).required() + override val runner get() = runnerOptions.runner + + override val barrierProducer = ::JvmSpinBarrier + override val affinityMapSchedule = listOf(null) + + private val allowJCStressReruns by option("--allow-jcs-reruns") + .flag() + + override fun run() = if (runner is JCStressRunner) jcstressRun() else super.run() + + private fun jcstressRun() { + if (listOnly) { + runListOnly() + return + } + + val paramsList = variateRunParams( + batchSizeSchedule = batchSizeSchedule, + affinityMapSchedule = affinityMapSchedule, + syncPeriodSchedule = syncEverySchedule, + barrierSchedule = listOf(barrierProducer), + ).toList() + when (paramsList.size) { + 0 -> { + echo("parameters list is empty; ensure no empty lists are used", err = true) + return + } + + 1 -> {} // ok + else -> { + if (!allowJCStressReruns) { + echo( + "you likely don't want to run JCStress multiple times;" + + " if you're sure, enable --allow-jcs-reruns", + err = true + ) + return + } + } + } + + for (params in paramsList) { + val jcsParams = if ( + params.batchSize == DEFAULT_BATCH_SIZE && + params.syncPeriod == DEFAULT_SYNC_EVERY + ) JCStressRunner.DEFAULT_LITMUSKT_PARAMS else params // jcstress defaults are different + + val jcsRunner = runner as JCStressRunner // use the correct runTests()! + val results = jcsRunner.runTests(tests, jcsParams).first() + echo("\n" + results.generateTable()) + } + } +} diff --git a/cli/src/nativeMain/kotlin/NativeMain.kt b/cli/src/nativeMain/kotlin/NativeMain.kt new file mode 100755 index 0000000..b5ef711 --- /dev/null +++ b/cli/src/nativeMain/kotlin/NativeMain.kt @@ -0,0 +1,4 @@ +import org.jetbrains.litmuskt.CliNative +import org.jetbrains.litmuskt.commonMain + +fun main(args: Array) = commonMain(args, CliNative()) diff --git a/litmus/src/nativeMain/kotlin/komem.litmus/CliNative.kt b/cli/src/nativeMain/kotlin/org/jetbrains/litmuskt/CliNative.kt similarity index 58% rename from litmus/src/nativeMain/kotlin/komem.litmus/CliNative.kt rename to cli/src/nativeMain/kotlin/org/jetbrains/litmuskt/CliNative.kt index 066c726..178f4bd 100644 --- a/litmus/src/nativeMain/kotlin/komem.litmus/CliNative.kt +++ b/cli/src/nativeMain/kotlin/org/jetbrains/litmuskt/CliNative.kt @@ -1,16 +1,22 @@ -package komem.litmus +package org.jetbrains.litmuskt import com.github.ajalt.clikt.parameters.options.default import com.github.ajalt.clikt.parameters.options.option import com.github.ajalt.clikt.parameters.types.choice -import komem.litmus.barriers.CinteropSpinBarrier +import org.jetbrains.litmuskt.CliCommon +import org.jetbrains.litmuskt.barriers.CinteropSpinBarrier +import org.jetbrains.litmuskt.PthreadRunner +import org.jetbrains.litmuskt.WorkerRunner +import org.jetbrains.litmuskt.AffinityMap class CliNative : CliCommon() { - override val runner = WorkerRunner + override val runner by option("-r", "--runner") + .choice(mapOf("worker" to WorkerRunner(), "pthread" to PthreadRunner())) + .default(WorkerRunner()) private val affinityMapChoices = run { val schedulesMapped = mutableMapOf>("none" to listOf(null)) - getAffinityManager()?.let { + org.jetbrains.litmuskt.getAffinityManager()?.let { schedulesMapped["short"] = it.presetShort() schedulesMapped["long"] = it.presetLong() } diff --git a/codegen/build.gradle.kts b/codegen/build.gradle.kts index ef3b11e..2fff865 100644 --- a/codegen/build.gradle.kts +++ b/codegen/build.gradle.kts @@ -1,20 +1,7 @@ plugins { - kotlin("multiplatform") + kotlin("jvm") } -group = "com.example" -version = "1.0-SNAPSHOT" - -kotlin { - jvm() - sourceSets { - val jvmMain by getting { - dependencies { - implementation("com.google.devtools.ksp:symbol-processing-api:1.9.10-1.0.13") -// implementation(project(":litmus")) - } - kotlin.srcDir("src/main/kotlin") - resources.srcDir("src/main/resources") - } - } +dependencies { + implementation("com.google.devtools.ksp:symbol-processing-api:1.9.10-1.0.13") } diff --git a/codegen/src/main/kotlin/komem/litmus/LitmusTestProcessor.kt b/codegen/src/main/kotlin/komem/litmus/LitmusTestProcessor.kt deleted file mode 100644 index fc16a44..0000000 --- a/codegen/src/main/kotlin/komem/litmus/LitmusTestProcessor.kt +++ /dev/null @@ -1,58 +0,0 @@ -package komem.litmus - -import com.google.devtools.ksp.processing.* -import com.google.devtools.ksp.symbol.KSAnnotated -import com.google.devtools.ksp.symbol.KSPropertyDeclaration - -class LitmusTestProcessorProvider : SymbolProcessorProvider { - override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { - return LitmusTestProcessor(environment.codeGenerator) - } -} - -class LitmusTestProcessor(val codeGenerator: CodeGenerator) : SymbolProcessor { - override fun process(resolver: Resolver): List { - val basePackage = "komem.litmus" - val registryFileName = "LitmusTestRegistry" - - val testFiles = resolver.getAllFiles().filter { it.packageName.asString() == "$basePackage.testsuite" }.toList() - val dependencies = Dependencies(true, *testFiles.toTypedArray()) - - val registryFile = try { - codeGenerator.createNewFile(dependencies, "$basePackage.generated", registryFileName) - } catch (e: FileAlreadyExistsException) { // TODO: this is a workaround - return emptyList() - } - - val decls = testFiles.flatMap { it.declarations }.filterIsInstance() - val namedTestsMap = decls.associate { - val relativePackage = it.packageName.asString().removePrefix("$basePackage.testsuite") - val testAlias = (if (relativePackage.isEmpty()) "" else "$relativePackage.") + - it.containingFile!!.fileName.removeSuffix(".kt") + - "." + it.simpleName.getShortName() - val testName = it.qualifiedName!!.asString() - testAlias to testName - } - - val registryCode = """ -package $basePackage.generated -import $basePackage.LitmusTest - -object LitmusTestRegistry { - private val tests: Set>> = setOf( - ${namedTestsMap.entries.joinToString(",\n" + " ".repeat(8)) { (a, n) -> "\"$a\" to $n" }} - ) - - operator fun get(regex: Regex) = tests.filter { regex.matches(it.first) }.map { it.second } - - fun all() = tests.map { it.second } - - fun resolveName(test: LitmusTest<*>) = tests.firstOrNull { it.second == test }?.first ?: "" -} - - """.trimIndent() - - registryFile.write(registryCode.toByteArray()) - return emptyList() - } -} diff --git a/codegen/src/main/kotlin/org/jetbrains/litmuskt/LitmusTestProcessor.kt b/codegen/src/main/kotlin/org/jetbrains/litmuskt/LitmusTestProcessor.kt new file mode 100644 index 0000000..2ebbed5 --- /dev/null +++ b/codegen/src/main/kotlin/org/jetbrains/litmuskt/LitmusTestProcessor.kt @@ -0,0 +1,94 @@ +package org.jetbrains.litmuskt + +import com.google.devtools.ksp.processing.* +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration + +class LitmusTestProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return LitmusTestProcessor(environment.codeGenerator, environment.logger) + } +} + +private const val testContainerAnnotationFQN = "org.jetbrains.litmuskt.LitmusTestContainer" +private const val basePackage = "org.jetbrains.litmuskt" +private const val generatedPackage = "$basePackage.generated" +private const val registryFileName = "LitmusTestRegistry" + +class LitmusTestProcessor( + private val codeGenerator: CodeGenerator, + private val logger: KSPLogger +) : SymbolProcessor { + + override fun process(resolver: Resolver): List { + val containerClassDecls = resolver.getSymbolsWithAnnotation(testContainerAnnotationFQN) + .filterIsInstance() + .toList() + // check for name collisions + containerClassDecls + .groupingBy { it.simpleName.asString() } + .eachCount() + .filter { it.value > 1 } + .takeIf { it.isNotEmpty() } + ?.let { logger.error("container class name collision: $it") } + // check that all containers are objects + containerClassDecls + .filterNot { it.classKind == ClassKind.OBJECT } + .takeIf { it.isNotEmpty() } + ?.let { logger.error("container class must be an object: $it") } + + val decls = containerClassDecls + .flatMap { it.getAllProperties() } + .filter { it.type.resolve().declaration.simpleName.asString() == "LitmusTest" } + .toList() + // should prevent extra rounds, or else createNewFile() will throw + if (decls.isEmpty()) return emptyList() + + val inputFiles = decls.mapNotNull { it.containingFile }.toSet() + val dependencies = Dependencies(true, *inputFiles.toTypedArray()) + val registryFile = codeGenerator.createNewFile(dependencies, generatedPackage, registryFileName) + + val namedTestsMap = decls.associate { + val testAlias = run { + val parentClassDecl = it.parentDeclaration + ?: error("test declaration at ${it.location} has no parent container class") + parentClassDecl.simpleName.asString() + "." + it.simpleName.asString() + } + val testFQN = it.qualifiedName!!.asString() + testAlias to testFQN + } + + val registryCode = """ +package $generatedPackage +import $basePackage.LitmusTest + +actual object LitmusTestRegistry { + + private data class TestData( + val alias: String, + val fqn: String, + ) + + private val tests: Map, TestData> = mapOf( + ${ + namedTestsMap.entries.joinToString(",\n" + " ".repeat(8)) { (alias, fqn) -> + "$fqn to TestData(\"$alias\", \"$fqn\")" + } + } + ) + + actual operator fun get(regex: Regex): List> = + tests.entries.filter { regex.matches(it.value.alias) }.map { it.key } + + actual fun all(): List> = tests.keys.toList() + actual fun getAlias(test: LitmusTest<*>): String = tests[test]?.alias ?: error("unknown test") + actual fun getFQN(test: LitmusTest<*>): String = tests[test]?.fqn ?: error("unknown test") +} + + """.trimIndent() + + registryFile.write(registryCode.toByteArray()) + return emptyList() + } +} diff --git a/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider index 693bed9..8e4e936 100644 --- a/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider +++ b/codegen/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider @@ -1 +1 @@ -komem.litmus.LitmusTestProcessorProvider \ No newline at end of file +org.jetbrains.litmuskt.LitmusTestProcessorProvider \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..0aed6df --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,50 @@ +plugins { + kotlin("multiplatform") + `java-library` +} + +kotlin { + val nativeTargets = listOf( + linuxX64(), + // 1) no machine currently available 2) CLI library does not support +// linuxArm64(), + macosX64(), + macosArm64(), + ) + + jvm { + withJava() + jvmToolchain(17) + } + + val hostOs = System.getProperty("os.name") + val affinitySupported = hostOs == "Linux" + nativeTargets.forEach { target -> + target.apply { + compilations.getByName("main") { + cinterops { + create("barrier") { + defFile(project.file("src/nativeInterop/barrier.def")) + headers(project.file("src/nativeInterop/barrier.h")) + } + if (affinitySupported) { + create("affinity") { + defFile(project.file("src/nativeInterop/kaffinity.def")) + compilerOpts.add("-D_GNU_SOURCE") + } + } + create("kpthread") { + defFile(project.file("src/nativeInterop/kpthread.def")) + } + } + } + } + } + sourceSets { + commonTest { + dependencies { + implementation(kotlin("test")) + } + } + } +} diff --git a/litmus/src/commonMain/kotlin/komem/litmus/AffinityMap.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/AffinityMap.kt similarity index 72% rename from litmus/src/commonMain/kotlin/komem/litmus/AffinityMap.kt rename to core/src/commonMain/kotlin/org/jetbrains/litmuskt/AffinityMap.kt index bcdf0e1..7e9387c 100644 --- a/litmus/src/commonMain/kotlin/komem/litmus/AffinityMap.kt +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/AffinityMap.kt @@ -1,4 +1,4 @@ -package komem.litmus +package org.jetbrains.litmuskt fun interface AffinityMap { fun allowedCores(threadIndex: Int): Set diff --git a/litmus/src/commonMain/kotlin/komem/litmus/barriers/Barrier.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Barrier.kt similarity index 73% rename from litmus/src/commonMain/kotlin/komem/litmus/barriers/Barrier.kt rename to core/src/commonMain/kotlin/org/jetbrains/litmuskt/Barrier.kt index 91385e9..029c454 100644 --- a/litmus/src/commonMain/kotlin/komem/litmus/barriers/Barrier.kt +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Barrier.kt @@ -1,4 +1,4 @@ -package komem.litmus.barriers +package org.jetbrains.litmuskt interface Barrier { fun await() diff --git a/litmus/src/commonMain/kotlin/komem/litmus/LitmusOutcomeStats.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusOutcomeStats.kt similarity index 71% rename from litmus/src/commonMain/kotlin/komem/litmus/LitmusOutcomeStats.kt rename to core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusOutcomeStats.kt index 96e7a83..15812f1 100644 --- a/litmus/src/commonMain/kotlin/komem/litmus/LitmusOutcomeStats.kt +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusOutcomeStats.kt @@ -1,4 +1,4 @@ -package komem.litmus +package org.jetbrains.litmuskt typealias LitmusOutcome = Any? @@ -22,44 +22,37 @@ data class LitmusOutcomeSpec( in forbidden -> LitmusOutcomeType.FORBIDDEN else -> default } + + val all = accepted + interesting + forbidden } /** - * For convenience, it is possible to use `accept(vararg values)` if test outcome is a `List`. - * This is true for [LitmusAutoOutcome]. + * For convenience, it is possible to use `accept(r1, r2, ...)` if outcome is a [LitmusAutoOutcome]. + * In other cases, use `accept(setOf(...))` to accept one or many values. Note that to accept an iterable, + * it has to be wrapped in an extra `setOf()`. All of this applies as well to `interesting()` and `forbid()`. * - * Use `accept(value)` otherwise. Notice that `accept(a, b)` is NOT the same as `accept(a); accept(b)`. + * See [LitmusAutoOutcome] file for those extension functions. The generic is used precisely for them. * - * The same applies to `interesting()` and `forbid()`. + * single values are handled differently !!!!!!!!!!!!! TODO */ -class LitmusOutcomeSpecScope { +class LitmusOutcomeSpecScope { private val accepted = mutableSetOf() private val interesting = mutableSetOf() private val forbidden = mutableSetOf() private var default: LitmusOutcomeType? = null - fun accept(outcome: LitmusOutcome) { - accepted.add(outcome) - } - - fun accept(vararg outcome: LitmusOutcome) { - accepted.add(outcome.toList()) - } - - fun interesting(outcome: LitmusOutcome) { - interesting.add(outcome) - } + // note: if S is LitmusIOutcome, even single values should be interpreted as r1 - fun interesting(vararg outcome: LitmusOutcome) { - interesting.add(outcome.toList()) + fun accept(outcomes: Iterable) { + accepted.addAll(outcomes) } - fun forbid(outcome: LitmusOutcome) { - forbidden.add(outcome) + fun interesting(outcomes: Iterable) { + interesting.addAll(outcomes) } - fun forbid(vararg outcome: LitmusOutcome) { - forbidden.add(outcome.toList()) + fun forbid(outcomes: Iterable) { + forbidden.addAll(outcomes) } fun default(outcomeType: LitmusOutcomeType) { diff --git a/litmus/src/commonMain/kotlin/komem/litmus/LitmusRunParams.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunParams.kt similarity index 93% rename from litmus/src/commonMain/kotlin/komem/litmus/LitmusRunParams.kt rename to core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunParams.kt index 411c6d8..1e56251 100644 --- a/litmus/src/commonMain/kotlin/komem/litmus/LitmusRunParams.kt +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunParams.kt @@ -1,6 +1,4 @@ -package komem.litmus - -import komem.litmus.barriers.BarrierProducer +package org.jetbrains.litmuskt data class LitmusRunParams( val batchSize: Int, diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunner.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunner.kt new file mode 100644 index 0000000..9bbf193 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunner.kt @@ -0,0 +1,138 @@ +package org.jetbrains.litmuskt + +import kotlin.time.Duration +import kotlin.time.TimeSource + +abstract class LitmusRunner { + + /** + * Starts threads for the test and returns a "join handle". This handle should block + * until the threads join and then collect and return the results. + */ + protected abstract fun startTest( + test: LitmusTest, + states: Array, + barrierProducer: BarrierProducer, + syncPeriod: Int, + affinityMap: AffinityMap?, + ): () -> LitmusResult + + /** + * Entry point for running tests. This method can be overridden in case that particular runner + * does not need to allocate states. + */ + open fun startTest(test: LitmusTest, params: LitmusRunParams): () -> LitmusResult { + val states = TypedArray(params.batchSize) { test.stateProducer() } + return startTest(test, states, params.barrierProducer, params.syncPeriod, params.affinityMap) + } + + /** + * Entry point for running tests in parallel. Again, can be overridden in case a particular runner + * implements parallel runs in a different manner. + * + * Note: default implementation interprets AffinityMap as a sequence of smaller maps. + * Example: for a map [ [0], [1], [2], [3] ],a test with 2 threads, and 2 instances, the + * first instance will have a [ [0], [1] ] map and the second one will have [ [2], [3] ]. + */ + open fun LitmusRunner.startTestParallel( + test: LitmusTest, + params: LitmusRunParams, + instances: Int, + ): List<() -> LitmusResult> { + // separated due to allocations severely impacting threads + val allStates = List(instances) { + TypedArray(params.batchSize) { test.stateProducer() } + } + val allJoinHandles = List(instances) { instanceIndex -> + val newAffinityMap = params.affinityMap?.let { oldMap -> + AffinityMap { threadIndex -> + oldMap.allowedCores(instanceIndex * test.threadCount + threadIndex) + } + } + startTest( + test = test, + states = allStates[instanceIndex], + barrierProducer = params.barrierProducer, + syncPeriod = params.syncPeriod, + affinityMap = newAffinityMap, + ) + } + return allJoinHandles + } + + protected fun calcStats( + states: Array, + spec: LitmusOutcomeSpec, + outcomeFinalizer: (S) -> LitmusOutcome + ): LitmusResult { + // cannot do `map.getOrPut(key){0L}++` with Long-s, and by getting rid of one + // extra put(), we are also getting rid of one extra hashCode() + class LongHolder(var value: Long) + + // the absolute majority of outcomes will be declared in spec + val specifiedOutcomes = (spec.accepted + spec.interesting + spec.forbidden).toTypedArray() + val specifiedCounts = Array(specifiedOutcomes.size) { 0L } + val useFastPath = specifiedOutcomes.size <= 10 + + val totalCounts = mutableMapOf() + + for (s in states) { + val outcome = outcomeFinalizer(s) + if (useFastPath) { + val i = specifiedOutcomes.indexOf(outcome) + if (i != -1) { + specifiedCounts[i]++ + continue + } + } + totalCounts.getOrPut(outcome) { LongHolder(0L) }.value++ + } + // update totalCounts with fastPathCounts + for (i in specifiedCounts.indices) { + val count = specifiedCounts[i] + if (count > 0) totalCounts + .getOrPut(specifiedOutcomes[i]) { LongHolder(0L) } + .value = count + } + + return totalCounts.map { (outcome, count) -> + LitmusOutcomeStats(outcome, count.value, spec.getType(outcome)) + } + } +} + +/** + * Runs [test] with [params], [timeLimit] and in parallel [instances]. + * + * If [timeLimit] is not given, run the test once. If [instances] is not given, use as + * many as possible without overlapping CPU cores between instances. + */ +fun LitmusRunner.runSingleTestParallel( + test: LitmusTest, + params: LitmusRunParams, + timeLimit: Duration = Duration.ZERO, + instances: Int = cpuCount() / test.threadCount, +): LitmusResult = repeatFor(timeLimit) { + startTestParallel(test, params, instances).map { it() }.mergeResults() +}.mergeResults() + +/** + * Runs [tests] one by one, each with [params] and [timeLimit]. + * + * If [timeLimit] is not given, run each test once. + */ +fun LitmusRunner.runTests( + tests: List>, + params: LitmusRunParams, + timeLimit: Duration = Duration.ZERO, +): List = tests.map { test -> + repeatFor(timeLimit) { startTest(test, params).invoke() }.mergeResults() +} + +// guaranteed to run [f] at least once +private inline fun repeatFor(duration: Duration, crossinline f: () -> T): List = buildList { + val start = TimeSource.Monotonic.markNow() + do { + add(f()) + } while (start.elapsedNow() < duration) +} diff --git a/litmus/src/commonMain/kotlin/komem/litmus/LitmusTest.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTest.kt similarity index 68% rename from litmus/src/commonMain/kotlin/komem/litmus/LitmusTest.kt rename to core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTest.kt index bd04a6a..4404f01 100644 --- a/litmus/src/commonMain/kotlin/komem/litmus/LitmusTest.kt +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTest.kt @@ -1,20 +1,22 @@ -package komem.litmus +package org.jetbrains.litmuskt -data class LitmusTest( +import org.jetbrains.litmuskt.autooutcomes.LitmusAutoOutcome + +data class LitmusTest( val stateProducer: () -> S, - val threadFunctions: List Any?>, + val threadFunctions: List Unit>, val outcomeFinalizer: (S.() -> LitmusOutcome), val outcomeSpec: LitmusOutcomeSpec ) { val threadCount = threadFunctions.size } -class LitmusTestScope( +class LitmusTestScope( private val stateProducer: () -> S ) { - private val threadFunctions = mutableListOf Any?>() + private val threadFunctions = mutableListOf Unit>() private lateinit var outcomeFinalizer: S.() -> LitmusOutcome - private lateinit var outcomeSpec: LitmusOutcomeSpecScope + private lateinit var outcomeSpec: LitmusOutcomeSpecScope fun thread(function: S.() -> Unit) { threadFunctions.add(function) @@ -25,9 +27,9 @@ class LitmusTestScope( outcomeFinalizer = function } - fun spec(setup: LitmusOutcomeSpecScope.() -> Unit) { + fun spec(setup: LitmusOutcomeSpecScope.() -> Unit) { if (::outcomeSpec.isInitialized) error("cannot set spec more than once") - outcomeSpec = LitmusOutcomeSpecScope().apply(setup) + outcomeSpec = LitmusOutcomeSpecScope().apply(setup) } fun build(): LitmusTest { @@ -36,7 +38,7 @@ class LitmusTestScope( val outcomeFinalizer: S.() -> LitmusOutcome = when { ::outcomeFinalizer.isInitialized -> outcomeFinalizer stateProducer() is LitmusAutoOutcome -> { - { (this as LitmusAutoOutcome).getOutcome() } + { this } } else -> error("outcome not specified") @@ -45,5 +47,5 @@ class LitmusTestScope( } } -fun litmusTest(stateProducer: () -> S, setup: LitmusTestScope.() -> Unit) = +fun litmusTest(stateProducer: () -> S, setup: LitmusTestScope.() -> Unit): LitmusTest<*> = LitmusTestScope(stateProducer).apply(setup).build() diff --git a/litmus/src/commonMain/kotlin/komem/litmus/Utils.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Utils.kt similarity index 85% rename from litmus/src/commonMain/kotlin/komem/litmus/Utils.kt rename to core/src/commonMain/kotlin/org/jetbrains/litmuskt/Utils.kt index 647df87..3bd7d76 100644 --- a/litmus/src/commonMain/kotlin/komem/litmus/Utils.kt +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Utils.kt @@ -1,4 +1,4 @@ -package komem.litmus +package org.jetbrains.litmuskt fun List>.tableFormat(hasHeader: Boolean = false): String { val columnCount = maxOf { it.size } @@ -24,3 +24,6 @@ fun List>.tableFormat(hasHeader: Boolean = false): String { } expect fun cpuCount(): Int + +@Suppress("UNCHECKED_CAST") +fun TypedArray(size: Int, init: (Int) -> S): Array = Array(size, init) as Array diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/BooleanAutoOutcomes.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/BooleanAutoOutcomes.kt new file mode 100644 index 0000000..4f1f83f --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/BooleanAutoOutcomes.kt @@ -0,0 +1,28 @@ +package org.jetbrains.litmuskt.autooutcomes + +import org.jetbrains.litmuskt.LitmusOutcomeSpecScope + +// TODO + +open class LitmusZZOutcome( + var r1: Boolean = false, + var r2: Boolean = false +) : LitmusAutoOutcome { + final override fun toString() = "($r1, $r2)" + final override fun hashCode() = (if (r1) 1 else 0) + (if (r2) 2 else 3) + final override fun equals(o: Any?): Boolean { + if (o !is LitmusZZOutcome) return false + return r1 == o.r1 && r2 == o.r2 + } + + final override fun toList() = listOf(r1, r2) +} + +fun LitmusOutcomeSpecScope.accept(r1: Boolean, r2: Boolean) = + accept(setOf(LitmusZZOutcome(r1, r2))) + +fun LitmusOutcomeSpecScope.interesting(r1: Boolean, r2: Boolean) = + interesting(setOf(LitmusZZOutcome(r1, r2))) + +fun LitmusOutcomeSpecScope.forbid(r1: Boolean, r2: Boolean) = + forbid(setOf(LitmusZZOutcome(r1, r2))) diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/IntAutoOutcomes.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/IntAutoOutcomes.kt new file mode 100644 index 0000000..9ecd149 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/IntAutoOutcomes.kt @@ -0,0 +1,97 @@ +package org.jetbrains.litmuskt.autooutcomes + +import org.jetbrains.litmuskt.LitmusOutcomeSpecScope + +open class LitmusIOutcome( + var r1: Int = 0, +) : LitmusAutoOutcome { + final override fun toString() = "$r1" + final override fun hashCode() = r1 + final override fun equals(o: Any?): Boolean { + if (o !is LitmusIOutcome) return false + return r1 == o.r1 + } + + final override fun toList() = listOf(r1) +} + +fun LitmusOutcomeSpecScope.accept(r1: Int) = + accept(setOf(LitmusIOutcome(r1))) + +fun LitmusOutcomeSpecScope.interesting(r1: Int) = + interesting(setOf(LitmusIOutcome(r1))) + +fun LitmusOutcomeSpecScope.forbid(r1: Int) = + forbid(setOf(LitmusIOutcome(r1))) + +open class LitmusIIOutcome( + var r1: Int = 0, + var r2: Int = 0 +) : LitmusAutoOutcome { + final override fun toString() = "($r1, $r2)" + final override fun hashCode() = (r1 shl 16) + r2 + final override fun equals(o: Any?): Boolean { + if (o !is LitmusIIOutcome) return false + return r1 == o.r1 && r2 == o.r2 + } + + final override fun toList() = listOf(r1, r2) +} + +fun LitmusOutcomeSpecScope.accept(r1: Int, r2: Int) = + accept(setOf(LitmusIIOutcome(r1, r2))) + +fun LitmusOutcomeSpecScope.interesting(r1: Int, r2: Int) = + interesting(setOf(LitmusIIOutcome(r1, r2))) + +fun LitmusOutcomeSpecScope.forbid(r1: Int, r2: Int) = + forbid(setOf(LitmusIIOutcome(r1, r2))) + +open class LitmusIIIOutcome( + var r1: Int = 0, + var r2: Int = 0, + var r3: Int = 0, +) : LitmusAutoOutcome { + final override fun toString() = "($r1, $r2, $r3)" + final override fun hashCode() = (r1 shl 20) + (r2 shl 10) + r3 + final override fun equals(o: Any?): Boolean { + if (o !is LitmusIIIOutcome) return false + return r1 == o.r1 && r2 == o.r2 && r3 == o.r3 + } + + final override fun toList() = listOf(r1, r2, r3) +} + +fun LitmusOutcomeSpecScope.accept(r1: Int, r2: Int, r3: Int) = + accept(setOf(LitmusIIIOutcome(r1, r2, r3))) + +fun LitmusOutcomeSpecScope.interesting(r1: Int, r2: Int, r3: Int) = + interesting(setOf(LitmusIIIOutcome(r1, r2, r3))) + +fun LitmusOutcomeSpecScope.forbid(r1: Int, r2: Int, r3: Int) = + forbid(setOf(LitmusIIIOutcome(r1, r2, r3))) + +open class LitmusIIIIOutcome( + var r1: Int = 0, + var r2: Int = 0, + var r3: Int = 0, + var r4: Int = 0, +) : LitmusAutoOutcome { + final override fun toString() = "($r1, $r2, $r3, $r4)" + final override fun hashCode() = (r1 shl 24) + (r2 shl 16) + (r3 shl 8) + r4 + final override fun equals(o: Any?): Boolean { + if (o !is LitmusIIIIOutcome) return false + return r1 == o.r1 && r2 == o.r2 && r3 == o.r3 && r4 == o.r4 + } + + final override fun toList() = listOf(r1, r2, r3, r4) +} + +fun LitmusOutcomeSpecScope.accept(r1: Int, r2: Int, r3: Int, r4: Int) = + accept(setOf(LitmusIIIIOutcome(r1, r2, r3, r4))) + +fun LitmusOutcomeSpecScope.interesting(r1: Int, r2: Int, r3: Int, r4: Int) = + interesting(setOf(LitmusIIIIOutcome(r1, r2, r3, r4))) + +fun LitmusOutcomeSpecScope.forbid(r1: Int, r2: Int, r3: Int, r4: Int) = + forbid(setOf(LitmusIIIIOutcome(r1, r2, r3, r4))) diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LitmusAutoOutcome.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LitmusAutoOutcome.kt new file mode 100644 index 0000000..ce26e27 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LitmusAutoOutcome.kt @@ -0,0 +1,29 @@ +package org.jetbrains.litmuskt.autooutcomes + +import org.jetbrains.litmuskt.LitmusOutcome + +/** + * A convenience interface to simplify specifying outcomes. + * + * All classes implementing this interface provide some r1, r2, ... variables + * to write the outcome into. If a litmus test's state extends one of these classes, + * specifying `outcome { ... }` is not necessary, as it will be inferred from r1, r2, ... + * + * Children classes should override `hashCode()` and `equals()` so that they are compared + * based on their outcome only. They should also override `toString()` so that they only display + * their outcome when printed. For these reasons the functions are overridden in this + * interface such that their implementation is forced in children. + * + * These classes are also used as outcomes themselves in order to better utilize resources. + */ +sealed interface LitmusAutoOutcome { + override fun toString(): String + override fun hashCode(): Int + + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun equals(o: Any?): Boolean + + // for JCStress interop + fun toList(): List +} + diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LongAutoOutcomes.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LongAutoOutcomes.kt new file mode 100644 index 0000000..4d5e759 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LongAutoOutcomes.kt @@ -0,0 +1,97 @@ +package org.jetbrains.litmuskt.autooutcomes + +import org.jetbrains.litmuskt.LitmusOutcomeSpecScope + +open class LitmusLOutcome( + var r1: Long = 0, +) : LitmusAutoOutcome { + final override fun toString() = "$r1" + final override fun hashCode() = r1.toInt() + final override fun equals(o: Any?): Boolean { + if (o !is LitmusLOutcome) return false + return r1 == o.r1 + } + + final override fun toList() = listOf(r1) +} + +fun LitmusOutcomeSpecScope.accept(r1: Long) = + accept(setOf(LitmusLOutcome(r1))) + +fun LitmusOutcomeSpecScope.interesting(r1: Long) = + interesting(setOf(LitmusLOutcome(r1))) + +fun LitmusOutcomeSpecScope.forbid(r1: Long) = + forbid(setOf(LitmusLOutcome(r1))) + +open class LitmusLLOutcome( + var r1: Long = 0, + var r2: Long = 0 +) : LitmusAutoOutcome { + final override fun toString() = "($r1, $r2)" + final override fun hashCode() = ((r1 shl 16) + r2).toInt() + final override fun equals(o: Any?): Boolean { + if (o !is LitmusLLOutcome) return false + return r1 == o.r1 && r2 == o.r2 + } + + final override fun toList() = listOf(r1, r2) +} + +fun LitmusOutcomeSpecScope.accept(r1: Long, r2: Long) = + accept(setOf(LitmusLLOutcome(r1, r2))) + +fun LitmusOutcomeSpecScope.interesting(r1: Long, r2: Long) = + interesting(setOf(LitmusLLOutcome(r1, r2))) + +fun LitmusOutcomeSpecScope.forbid(r1: Long, r2: Long) = + forbid(setOf(LitmusLLOutcome(r1, r2))) + +open class LitmusLLLOutcome( + var r1: Long = 0, + var r2: Long = 0, + var r3: Long = 0, +) : LitmusAutoOutcome { + final override fun toString() = "($r1, $r2, $r3)" + final override fun hashCode() = ((r1 shl 20) + (r2 shl 10) + r3).toInt() + final override fun equals(o: Any?): Boolean { + if (o !is LitmusLLLOutcome) return false + return r1 == o.r1 && r2 == o.r2 && r3 == o.r3 + } + + final override fun toList() = listOf(r1, r2, r3) +} + +fun LitmusOutcomeSpecScope.accept(r1: Long, r2: Long, r3: Long) = + accept(setOf(LitmusLLLOutcome(r1, r2, r3))) + +fun LitmusOutcomeSpecScope.interesting(r1: Long, r2: Long, r3: Long) = + interesting(setOf(LitmusLLLOutcome(r1, r2, r3))) + +fun LitmusOutcomeSpecScope.forbid(r1: Long, r2: Long, r3: Long) = + forbid(setOf(LitmusLLLOutcome(r1, r2, r3))) + +open class LitmusLLLLOutcome( + var r1: Long = 0, + var r2: Long = 0, + var r3: Long = 0, + var r4: Long = 0, +) : LitmusAutoOutcome { + final override fun toString() = "($r1, $r2, $r3, $r4)" + final override fun hashCode() = ((r1 shl 24) + (r2 shl 16) + (r3 shl 8) + r4).toInt() + final override fun equals(o: Any?): Boolean { + if (o !is LitmusLLLLOutcome) return false + return r1 == o.r1 && r2 == o.r2 && r3 == o.r3 && r4 == o.r4 + } + + final override fun toList() = listOf(r1, r2, r3, r4) +} + +fun LitmusOutcomeSpecScope.accept(r1: Long, r2: Long, r3: Long, r4: Long) = + accept(setOf(LitmusLLLLOutcome(r1, r2, r3, r4))) + +fun LitmusOutcomeSpecScope.interesting(r1: Long, r2: Long, r3: Long, r4: Long) = + interesting(setOf(LitmusLLLLOutcome(r1, r2, r3, r4))) + +fun LitmusOutcomeSpecScope.forbid(r1: Long, r2: Long, r3: Long, r4: Long) = + forbid(setOf(LitmusLLLLOutcome(r1, r2, r3, r4))) diff --git a/litmus/src/commonTest/kotlin/komem.litmus/LitmusOutcomeTest.kt b/core/src/commonTest/kotlin/org/jetbrains/litmuskt/LitmusOutcomeTest.kt similarity index 88% rename from litmus/src/commonTest/kotlin/komem.litmus/LitmusOutcomeTest.kt rename to core/src/commonTest/kotlin/org/jetbrains/litmuskt/LitmusOutcomeTest.kt index da0a8fb..b7fe426 100644 --- a/litmus/src/commonTest/kotlin/komem.litmus/LitmusOutcomeTest.kt +++ b/core/src/commonTest/kotlin/org/jetbrains/litmuskt/LitmusOutcomeTest.kt @@ -1,5 +1,8 @@ package komem.litmus +import org.jetbrains.litmuskt.LitmusOutcomeStats +import org.jetbrains.litmuskt.LitmusOutcomeType +import org.jetbrains.litmuskt.mergeResults import kotlin.test.Test import kotlin.test.assertEquals diff --git a/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmThreadRunner.kt b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmThreadRunner.kt new file mode 100644 index 0000000..e034edf --- /dev/null +++ b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmThreadRunner.kt @@ -0,0 +1,36 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.* +import org.jetbrains.litmuskt.* + +/** + * A simplistic runner based on JVM threads. Does not support affinity. + */ +class JvmThreadRunner : LitmusRunner() { + + override fun startTest( + test: LitmusTest, + states: Array, + barrierProducer: BarrierProducer, + syncPeriod: Int, + affinityMap: AffinityMap? + ): () -> LitmusResult { + val barrier = barrierProducer(test.threadCount) + + val threads = List(test.threadCount) { threadIndex -> + Thread { + val threadFunction = test.threadFunctions[threadIndex] + for (i in states.indices) { + if (i % syncPeriod == 0) barrier.await() + states[i].threadFunction() + } + } + } + threads.forEach { it.start() } + + return { + threads.forEach { it.join() } + calcStats(states, test.outcomeSpec, test.outcomeFinalizer) + } + } +} diff --git a/litmus/src/jvmMain/kotlin/komem/litmus/JvmUtils.kt b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmUtils.kt similarity index 68% rename from litmus/src/jvmMain/kotlin/komem/litmus/JvmUtils.kt rename to core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmUtils.kt index 22e0392..4050958 100644 --- a/litmus/src/jvmMain/kotlin/komem/litmus/JvmUtils.kt +++ b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmUtils.kt @@ -1,3 +1,3 @@ -package komem.litmus +package org.jetbrains.litmuskt actual fun cpuCount() = Runtime.getRuntime().availableProcessors() diff --git a/litmus/src/jvmMain/kotlin/komem/litmus/barriers/JvmCyclicBarrier.kt b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/barriers/JvmCyclicBarrier.kt similarity index 72% rename from litmus/src/jvmMain/kotlin/komem/litmus/barriers/JvmCyclicBarrier.kt rename to core/src/jvmMain/kotlin/org/jetbrains/litmuskt/barriers/JvmCyclicBarrier.kt index 50f3918..a05efa3 100644 --- a/litmus/src/jvmMain/kotlin/komem/litmus/barriers/JvmCyclicBarrier.kt +++ b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/barriers/JvmCyclicBarrier.kt @@ -1,5 +1,6 @@ -package komem.litmus.barriers +package org.jetbrains.litmuskt.barriers +import org.jetbrains.litmuskt.Barrier import java.util.concurrent.CyclicBarrier class JvmCyclicBarrier(threadCount: Int) : Barrier { diff --git a/litmus/src/jvmMain/kotlin/komem/litmus/barriers/JvmSpinBarrier.kt b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/barriers/JvmSpinBarrier.kt similarity index 87% rename from litmus/src/jvmMain/kotlin/komem/litmus/barriers/JvmSpinBarrier.kt rename to core/src/jvmMain/kotlin/org/jetbrains/litmuskt/barriers/JvmSpinBarrier.kt index c4c5443..1375cc6 100644 --- a/litmus/src/jvmMain/kotlin/komem/litmus/barriers/JvmSpinBarrier.kt +++ b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/barriers/JvmSpinBarrier.kt @@ -1,5 +1,6 @@ -package komem.litmus.barriers +package org.jetbrains.litmuskt.barriers +import org.jetbrains.litmuskt.Barrier import java.util.concurrent.atomic.AtomicInteger class JvmSpinBarrier(private val threadCount: Int) : Barrier { diff --git a/core/src/linuxMain/kotlin/org/jetbrains/litmuskt/AffinityBindingsImplPosix.kt b/core/src/linuxMain/kotlin/org/jetbrains/litmuskt/AffinityBindingsImplPosix.kt new file mode 100644 index 0000000..d6e4fa8 --- /dev/null +++ b/core/src/linuxMain/kotlin/org/jetbrains/litmuskt/AffinityBindingsImplPosix.kt @@ -0,0 +1,42 @@ +package org.jetbrains.litmuskt + +import kaffinity.* +import org.jetbrains.litmuskt.AffinityManager +import org.jetbrains.litmuskt.syscallCheck +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.alloc +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.ptr +import platform.posix.cpu_set_t +import platform.posix.pthread_t +import kotlin.native.concurrent.ObsoleteWorkersApi +import kotlin.native.concurrent.Worker + +@OptIn(ExperimentalStdlibApi::class, ObsoleteWorkersApi::class) +actual fun getAffinityManager(): AffinityManager? = object : AffinityManager { + override fun setAffinity(w: Worker, cpus: Set) { + setAffinity(w.platformThreadId, cpus) + } + + override fun getAffinity(w: Worker): Set { + return getAffinity(w.platformThreadId) + } + + @OptIn(ExperimentalForeignApi::class) + override fun setAffinity(thread: pthread_t, cpus: Set): Unit = memScoped { + require(cpus.isNotEmpty()) + val set = alloc() + cpu_zero(set.ptr) + for (cpu in cpus) cpu_set(cpu, set.ptr) + set_affinity(thread, set.ptr).syscallCheck() + } + + @OptIn(ExperimentalForeignApi::class) + override fun getAffinity(thread: pthread_t): Set = memScoped { + val set = alloc() + get_affinity(thread, set.ptr).syscallCheck() + return (0.. int set_affinity(pthread_t thread, cpu_set_t* set) { return pthread_setaffinity_np(thread, sizeof(*set), set); @@ -19,5 +19,5 @@ int cpu_isset(int cpu, cpu_set_t* set) { CPU_ISSET(cpu, set); } int cpu_setsize() { - return CPU_SETSIZE; + return CPU_SETSIZE; } diff --git a/core/src/nativeInterop/kpthread.def b/core/src/nativeInterop/kpthread.def new file mode 100644 index 0000000..c661c44 --- /dev/null +++ b/core/src/nativeInterop/kpthread.def @@ -0,0 +1,36 @@ +--- +#include "pthread.h" +#include "stdlib.h" + +/** + * On different platforms `pthread_t` hides different types (unsigned long int on Linux, struct on Macos). + * These functions provide a way to use `pthread_t` in a unified way as a `void*`. + */ +void *k_pthread_t_alloc() +{ + return malloc(sizeof(pthread_t)); +} + +void k_pthread_t_free(void *ptr) +{ + free(ptr); +} + +int k_pthread_create(void *pthread_ptr, void *(*function)(void *), void *args) +{ + return pthread_create( + (pthread_t *)pthread_ptr, + NULL, + function, + args + ); +} + +int k_pthread_join(void *pthread_ptr, void *result) +{ + pthread_t thread = *((pthread_t *)pthread_ptr); + return pthread_join( + thread, + result + ); +} diff --git a/litmus/src/nativeMain/kotlin/komem.litmus/AffinityManager.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/AffinityManager.kt similarity index 86% rename from litmus/src/nativeMain/kotlin/komem.litmus/AffinityManager.kt rename to core/src/nativeMain/kotlin/org/jetbrains/litmuskt/AffinityManager.kt index 7cab03f..ce5ee0f 100644 --- a/litmus/src/nativeMain/kotlin/komem.litmus/AffinityManager.kt +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/AffinityManager.kt @@ -1,9 +1,11 @@ @file:OptIn(kotlin.native.concurrent.ObsoleteWorkersApi::class) -package komem.litmus +package org.jetbrains.litmuskt // TODO: add documentation +import org.jetbrains.litmuskt.AffinityMap +import platform.posix.pthread_t import kotlin.native.concurrent.Worker import kotlin.random.Random @@ -11,6 +13,9 @@ interface AffinityManager { fun setAffinity(w: Worker, cpus: Set) fun getAffinity(w: Worker): Set + fun setAffinity(thread: pthread_t, cpus: Set) + fun getAffinity(thread: pthread_t): Set + fun newShiftMap(shift: Int): AffinityMap = object : AffinityMap { private val cpus: List> @@ -55,4 +60,4 @@ interface AffinityManager { ) } -expect fun getAffinityManager(): AffinityManager? +expect fun getAffinityManager(): AffinityManager? \ No newline at end of file diff --git a/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/NativeUtils.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/NativeUtils.kt new file mode 100644 index 0000000..236036b --- /dev/null +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/NativeUtils.kt @@ -0,0 +1,18 @@ +package org.jetbrains.litmuskt + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix._SC_NPROCESSORS_ONLN +import platform.posix.errno +import platform.posix.strerror +import platform.posix.sysconf + +actual fun cpuCount(): Int = sysconf(_SC_NPROCESSORS_ONLN).toInt() + +@OptIn(ExperimentalForeignApi::class) +fun Int.syscallCheck() { + if (this != 0) { + val err = strerror(errno)!!.toKString() + throw IllegalStateException("syscall error: $err") + } +} diff --git a/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadRunner.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadRunner.kt new file mode 100644 index 0000000..23e5ce9 --- /dev/null +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadRunner.kt @@ -0,0 +1,77 @@ +package org.jetbrains.litmuskt + +import kotlinx.cinterop.* +import kpthread.k_pthread_create +import kpthread.k_pthread_join +import kpthread.k_pthread_t_alloc +import kpthread.k_pthread_t_free + +private class ThreadData( + val states: Array, + val function: (Any) -> Unit, + val syncPeriod: Int, + val barrier: Barrier, +) + +private fun threadRoutine(data: ThreadData): Unit = with(data) { + for (i in states.indices) { + function(states[i]) + if (i % syncPeriod == 0) barrier.await() + } +} + +/** + * A runner based on pthread API provided by C interop from stdlib. + */ +class PthreadRunner : LitmusRunner() { + + @OptIn(ExperimentalForeignApi::class) + override fun startTest( + test: LitmusTest, + states: Array, + barrierProducer: BarrierProducer, + syncPeriod: Int, + affinityMap: AffinityMap? + ): () -> LitmusResult { + val barrier = barrierProducer(test.threadCount) + + fun startThread(threadIndex: Int): Pair> { + val function: (Any) -> Unit = { state -> + @Suppress("UNCHECKED_CAST") + test.threadFunctions[threadIndex].invoke(state as S) + } + val threadData = ThreadData(states, function, syncPeriod, barrier) + val threadDataRef = StableRef.create(threadData) + + val pthreadPtr = k_pthread_t_alloc() ?: error("could not allocate pthread_t pointer") + k_pthread_create( + pthreadPtr, + staticCFunction { + val data = it!!.asStableRef>().get() + threadRoutine(data) + return@staticCFunction null + }, + threadDataRef.asCPointer() + ).syscallCheck() + + // TODO: I don't think there is a way to assign affinity before the thread starts (would be useful for MacOS) +// getAffinityManager()?.let { am -> +// val map = affinityMap?.allowedCores(threadIndex) ?: return@let +// am.setAffinity(pthreadVar.value, map) +// require(am.getAffinity(pthreadVar.value) == map) { "setting affinity failed" } +// } + return pthreadPtr to threadDataRef + } + + val threads = List(test.threadCount) { startThread(it) } + + return { + for ((pthreadPtr, threadDataRef) in threads) { + k_pthread_join(pthreadPtr, null).syscallCheck() + k_pthread_t_free(pthreadPtr) + threadDataRef.dispose() + } + calcStats(states, test.outcomeSpec, test.outcomeFinalizer) + } + } +} diff --git a/litmus/src/nativeMain/kotlin/komem.litmus/WorkerRunner.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerRunner.kt similarity index 52% rename from litmus/src/nativeMain/kotlin/komem.litmus/WorkerRunner.kt rename to core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerRunner.kt index b0373ed..39dbe3a 100644 --- a/litmus/src/nativeMain/kotlin/komem.litmus/WorkerRunner.kt +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerRunner.kt @@ -1,34 +1,39 @@ -package komem.litmus +package org.jetbrains.litmuskt -import komem.litmus.barriers.Barrier -import kotlin.experimental.ExperimentalNativeApi +import org.jetbrains.litmuskt.* +import org.jetbrains.litmuskt.* import kotlin.native.concurrent.ObsoleteWorkersApi import kotlin.native.concurrent.TransferMode import kotlin.native.concurrent.Worker -object WorkerRunner : LitmusRunner() { +/** + * A runner based on Kotlin/Native Workers. They are declared obsolete, + * but an alternative is yet to exist. + */ +class WorkerRunner : LitmusRunner() { - @OptIn(ObsoleteWorkersApi::class, ExperimentalNativeApi::class) - override fun runTest( - params: LitmusRunParams, + @OptIn(ObsoleteWorkersApi::class) + override fun startTest( test: LitmusTest, - ): LitmusResult { + states: Array, + barrierProducer: BarrierProducer, + syncPeriod: Int, + affinityMap: AffinityMap? + ): () -> LitmusResult { data class WorkerContext( - val states: List, + val states: Array, val threadFunction: S.() -> Any?, val syncPeriod: Int, val barrier: Barrier, ) - val states = List(params.batchSize) { test.stateProducer() } - val barrier = params.barrierProducer(test.threadCount) - val outcomeFinalizer = test.outcomeFinalizer + val barrier = barrierProducer(test.threadCount) val workers = List(test.threadCount) { Worker.start() } - workers.mapIndexed { threadIndex, worker -> - params.affinityMap?.let { affinityMap -> - getAffinityManager()?.run { + val futures = workers.mapIndexed { threadIndex, worker -> + affinityMap?.let { affinityMap -> + org.jetbrains.litmuskt.getAffinityManager()?.run { val cpuSet = affinityMap.allowedCores(threadIndex) setAffinity(worker, cpuSet) require(getAffinity(worker) == cpuSet) { "affinity setting failed" } @@ -37,7 +42,7 @@ object WorkerRunner : LitmusRunner() { val workerContext = WorkerContext( states, test.threadFunctions[threadIndex], - params.syncPeriod, + syncPeriod, barrier, ) worker.execute( @@ -49,12 +54,12 @@ object WorkerRunner : LitmusRunner() { states[i].threadFunction() } } - worker.requestTermination() - }.forEach { it.result } // await all workers + } - val outcomes = states.map { it.outcomeFinalizer() } - assert(outcomes.size == params.batchSize) - - return outcomes.calcStats(test.outcomeSpec) + return { + futures.forEach { it.result } // await all results + workers.forEach { it.requestTermination().result } // waits for all workers to stop + calcStats(states, test.outcomeSpec, test.outcomeFinalizer) + } } } diff --git a/litmus/src/nativeMain/kotlin/komem.litmus/barriers/CinteropSpinBarrier.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/barriers/CinteropSpinBarrier.kt similarity index 84% rename from litmus/src/nativeMain/kotlin/komem.litmus/barriers/CinteropSpinBarrier.kt rename to core/src/nativeMain/kotlin/org/jetbrains/litmuskt/barriers/CinteropSpinBarrier.kt index e2b1086..ccda413 100644 --- a/litmus/src/nativeMain/kotlin/komem.litmus/barriers/CinteropSpinBarrier.kt +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/barriers/CinteropSpinBarrier.kt @@ -1,8 +1,9 @@ -package komem.litmus.barriers +package org.jetbrains.litmuskt.barriers import barrier.CSpinBarrier import barrier.barrier_wait import barrier.create_barrier +import org.jetbrains.litmuskt.Barrier import kotlinx.cinterop.CPointer import kotlinx.cinterop.ExperimentalForeignApi diff --git a/litmus/src/nativeMain/kotlin/komem.litmus/barriers/KNativeSpinBarrier.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/barriers/KNativeSpinBarrier.kt similarity index 87% rename from litmus/src/nativeMain/kotlin/komem.litmus/barriers/KNativeSpinBarrier.kt rename to core/src/nativeMain/kotlin/org/jetbrains/litmuskt/barriers/KNativeSpinBarrier.kt index a5435a3..93f9de4 100644 --- a/litmus/src/nativeMain/kotlin/komem.litmus/barriers/KNativeSpinBarrier.kt +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/barriers/KNativeSpinBarrier.kt @@ -1,5 +1,6 @@ -package komem.litmus.barriers +package org.jetbrains.litmuskt.barriers +import org.jetbrains.litmuskt.Barrier import kotlin.concurrent.AtomicInt class KNativeSpinBarrier(private val threadCount: Int) : Barrier { diff --git a/gradle.properties b/gradle.properties index 0e30cfb..16c8225 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,10 @@ kotlin.code.style=official +kotlin.native.ignoreDisabledTargets=true +kotlin.mpp.enableCInteropCommonization=true +cliktVersion=4.2.2 +atomicfuVersion=0.23.2 + +# this one is required for jvmRun task; can also be passed as -DmainClass=JvmMainKt +mainClass=JvmMainKt # path to custom compiler dist #kotlin.native.home=./fresh-kn-compiler/kotlin-native-linux-x86_64-1.9.0-RC diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index aa991fc..e411586 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/jcstests/Custom_LB.java b/jcstress-tests/Custom_LB.java similarity index 100% rename from jcstests/Custom_LB.java rename to jcstress-tests/Custom_LB.java diff --git a/jcstress-wrapper/build.gradle.kts b/jcstress-wrapper/build.gradle.kts new file mode 100644 index 0000000..e0bafda --- /dev/null +++ b/jcstress-wrapper/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + kotlin("jvm") + application +} + +application { + mainClass = "MainKt" +} + +dependencies { + implementation(project(":core")) + implementation(project(":testsuite")) + implementation(kotlin("reflect")) +} + +val jcsDir = rootProject.layout.projectDirectory.dir("jcstress") + +tasks.register("copyCoreToJCStress") { + dependsOn(":core:jvmJar") + from(project(":core").layout.buildDirectory.file("libs/core-jvm-$version.jar")) + rename { "litmusktJvm-1.0.jar" } + into(jcsDir.dir("libs/org/jetbrains/litmuskt/litmusktJvm/1.0/")) + doFirst { + if (inputs.sourceFiles.isEmpty) throw GradleException("missing files to copy") + } +} + +tasks.register("copyTestsuiteToJCStress") { + dependsOn(":testsuite:jvmJar") + from(project(":testsuite").layout.buildDirectory.file("libs/testsuite-jvm-$version.jar")) + rename { "litmusktJvmTestsuite-1.0.jar" } + into(jcsDir.dir("libs/org/jetbrains/litmuskt/litmusktJvmTestsuite/1.0/")) + doFirst { + if (inputs.sourceFiles.isEmpty) throw GradleException("missing files to copy") + } +} + +tasks.register("cleanJCStress") { + delete(jcsDir.dir("generatedSrc"), jcsDir.dir("libs")) +} + +tasks.getByName("clean").finalizedBy("cleanJCStress") diff --git a/jcstress-wrapper/src/main/kotlin/Main.kt b/jcstress-wrapper/src/main/kotlin/Main.kt new file mode 100644 index 0000000..9c22548 --- /dev/null +++ b/jcstress-wrapper/src/main/kotlin/Main.kt @@ -0,0 +1,26 @@ +import org.jetbrains.litmuskt.generateWrapperFile +import org.jetbrains.litmuskt.generated.LitmusTestRegistry +import kotlin.io.path.ExperimentalPathApi +import kotlin.io.path.Path +import kotlin.io.path.deleteRecursively +import kotlin.io.path.div + +@OptIn(ExperimentalPathApi::class) +fun main() { + var successCnt = 0 + val allTests = LitmusTestRegistry.all() + val generatedSrc = jcstressDirectory / "generatedSrc" + runCatching { + generatedSrc.deleteRecursively() + } + for (test in allTests) { + val success = generateWrapperFile(test, generatedSrc) + if (success) successCnt++ + } + if (successCnt != allTests.size) { + System.err.println("WARNING: generated wrappers for $successCnt out of ${allTests.size} known tests") + } +} + +// TODO: this is very shaky, only works because all subprojects are on the same level +val jcstressDirectory = Path("../jcstress/") diff --git a/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/Codegen.kt b/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/Codegen.kt new file mode 100644 index 0000000..79ce4fa --- /dev/null +++ b/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/Codegen.kt @@ -0,0 +1,149 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.autooutcomes.LitmusAutoOutcome +import java.nio.file.Path +import kotlin.io.path.createParentDirectories +import kotlin.io.path.div +import kotlin.io.path.writeText +import kotlin.reflect.full.allSuperclasses +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.superclasses + +fun generateWrapperFile(test: LitmusTest<*>, generatedSrc: Path): Boolean { + val targetFile = run { + val targetFilePackageFolder = test.qualifiedName.split(".").dropLast(2).joinToString("/") + val targetFileClassName = test.javaClassName + ".java" + generatedSrc / "main" / targetFilePackageFolder / targetFileClassName + } + targetFile.createParentDirectories() + val targetCode = try { + generateWrapperCode(test) + } catch (e: Throwable) { + System.err.println("WARNING: could not generate wrapper for ${test.alias} because: ${e.message}") + return false + } + targetFile.writeText(targetCode) + return true +} + +private fun generateWrapperCode(test: LitmusTest<*>): String { + val stateClass = test.stateProducer()::class + require(stateClass.allSuperclasses.contains(LitmusAutoOutcome::class)) { + "to use JCStress, test state must extend some LitmusAutoOutcome (e.g. LitmusIIOutcome)" + } + + val autoOutcomeClassList = stateClass.superclasses.filter { it.isSubclassOf(LitmusAutoOutcome::class) } + require(autoOutcomeClassList.size == 1) { "test state should extend exactly one LitmusAutoOutcome" } + val outcomeTypeName = autoOutcomeClassList.first().simpleName!! + .removePrefix("Litmus") + .removeSuffix("Outcome") + val (outcomeVarType, outcomeVarCount) = when (outcomeTypeName) { + "I" -> "Integer" to 1 + "II" -> "Integer" to 2 + "III" -> "Integer" to 3 + "IIII" -> "Integer" to 4 + else -> error("unknown AutoOutcome type $outcomeTypeName") + } + + val javaTestGetter: String = run { + val (className, testName) = test.alias.split(".") + val getter = "get${testName.replaceFirstChar { it.uppercaseChar() }}()" + "$className.INSTANCE.$getter" + } + + val javaArbiterDecl: String = run { + val jcstressResultClassName = outcomeTypeName + "_Result" + """ +@Arbiter +public void a($jcstressResultClassName r) { + List<$outcomeVarType> result = (List<$outcomeVarType>) (Object) ((LitmusAutoOutcome) fA.invoke(state)).toList(); + ${List(outcomeVarCount) { "r.r${it + 1} = result.get($it);" }.joinToString("\n ")} +} + """.trim() + } + + val jcstressOutcomeDecls: String = run { + val outcomes = test.outcomeSpec.accepted.associateWith { "ACCEPTABLE" } + + test.outcomeSpec.interesting.associateWith { "ACCEPTABLE_INTERESTING" } + + test.outcomeSpec.forbidden.associateWith { "FORBIDDEN" } + + // since only AutoOutcome is allowed, the cast is safe + outcomes.map { (o, t) -> + val oId = (o as LitmusAutoOutcome).toList().joinToString(", ") + "@Outcome(id = \"$oId\", expect = $t)" + }.joinToString("\n") + } + + val jcstressDefaultOutcomeType = when (test.outcomeSpec.default) { + LitmusOutcomeType.ACCEPTED -> "ACCEPTABLE" + LitmusOutcomeType.FORBIDDEN -> "FORBIDDEN" + LitmusOutcomeType.INTERESTING -> "ACCEPTABLE_INTERESTING" + } + + val testParentClassFQN = test.qualifiedName.split(".").dropLast(1).joinToString(".") + + return wrapperCode( + test, + jcstressOutcomeDecls, + jcstressDefaultOutcomeType, + javaTestGetter, + javaArbiterDecl, + testParentClassFQN + ) +} + +private fun javaThreadFunctionDecl(index: Int) = + "private static final Function1 fT$index = test.getThreadFunctions().get($index);" + +private fun javaActorDecl(index: Int) = """ + @Actor + public void t$index() { + fT$index.invoke(state); + } + """.trimIndent() + +fun wrapperCode( + test: LitmusTest<*>, + jcstressOutcomeDecls: String, + jcstressDefaultOutcomeType: String, + javaTestGetter: String, + javaArbiterDecl: String, + testParentClassFQN: String, +) = """ +package ${test.qualifiedName.split(".").dropLast(2).joinToString(".")}; + +import org.jetbrains.litmuskt.*; +import org.jetbrains.litmuskt.autooutcomes.*; + +import $testParentClassFQN; + +import kotlin.Unit; +import kotlin.jvm.functions.Function1; +import java.util.List; + +import org.openjdk.jcstress.annotations.*; +import org.openjdk.jcstress.infra.results.*; + +import static org.openjdk.jcstress.annotations.Expect.*; + +@JCStressTest +@State +$jcstressOutcomeDecls +@Outcome(expect = $jcstressDefaultOutcomeType) +public class ${test.javaClassName} { + + private static final LitmusTest test = (LitmusTest) $javaTestGetter; + ${List(test.threadCount) { javaThreadFunctionDecl(it) }.joinToString("\n ")} + private static final Function1 fA = test.getOutcomeFinalizer(); + + public ${test.javaClassName}() {} + + public Object state = test.getStateProducer().invoke(); + + ${List(test.threadCount) { javaActorDecl(it).padded(4) }.joinToString("\n\n ")} + + ${javaArbiterDecl.padded(4)} +} +""".trimIndent() + +private fun String.padded(padding: Int) = replace("\n", "\n" + " ".repeat(padding)) diff --git a/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/JCStressRunner.kt b/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/JCStressRunner.kt new file mode 100644 index 0000000..1c286b2 --- /dev/null +++ b/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/JCStressRunner.kt @@ -0,0 +1,163 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.barriers.JvmCyclicBarrier +import java.nio.file.Files +import java.nio.file.Path +import kotlin.io.path.div +import kotlin.streams.asSequence + +/** + * Note that this 'runner' is severely different from all others. + */ +class JCStressRunner( + private val jcstressDirectory: Path, + private val jcstressFreeArgs: List, +) : LitmusRunner() { + + companion object { + val DEFAULT_LITMUSKT_PARAMS = LitmusRunParams(0, 0, null, ::JvmCyclicBarrier) + } + + override fun startTest( + test: LitmusTest, + states: Array, + barrierProducer: BarrierProducer, + syncPeriod: Int, + affinityMap: AffinityMap? + ): () -> LitmusResult { + throw NotImplementedError("jcstress runner should not be called with explicit params like this") + } + + override fun LitmusRunner.startTestParallel( + test: LitmusTest, + params: LitmusRunParams, + instances: Int + ): List<() -> LitmusResult> { + throw NotImplementedError( + "jcstress runs tests in parallel by default; asking for parallelism explicitly is meaningless" + ) + } + + internal fun startTests( + tests: List>, + params: LitmusRunParams + ): () -> List { + val mvn = ProcessBuilder("mvn", "install", "verify", "-U") + .directory(jcstressDirectory.toFile()) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + mvn.waitFor() + if (mvn.exitValue() != 0) { + error("mvn exited with code ${mvn.exitValue()}") + } + + val jcsParams = if (params != DEFAULT_LITMUSKT_PARAMS) { + arrayOf("strideSize", "${params.syncPeriod}", "strideCount", "${params.batchSize / params.syncPeriod}") + } else emptyArray() + val jcs = ProcessBuilder( + "java", + "-jar", + "target/jcstress.jar", + *(jcsParams + jcstressFreeArgs), + "-t", + tests.joinToString("|") { "(${it.javaClassName})" }, + ) + .directory(jcstressDirectory.toFile()) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + + return handle@{ + jcs.waitFor() + if (jcs.exitValue() != 0) error("jcstress exited with code ${jcs.exitValue()}") + return@handle tests.map { test -> parseJCStressResults(test) } + } + } + + override fun startTest(test: LitmusTest, params: LitmusRunParams): () -> LitmusResult { + val handle = startTests(listOf(test), params) + return { handle().first() } + } + + /** + * Parses JCStress HTML output file. Here is the expected structure of the file, using SB as an example: + * + * ... + * Observed States <-- read the number of observed states + * + * + * + * 0, 1 <-- read the observed states in this order + * 1, 0 + * 1, 1 + * ... + * + * OK + * 3 <-- read the number of times observed + * 1 + * 0 + * <-- these lines repeat per each configuration, so the results are summed in the end + * ... + */ + private fun parseJCStressResults(test: LitmusTest<*>): LitmusResult { + val resultsFile = jcstressDirectory / "results" / "${test.javaFQN}.html" + var lines = Files.lines(resultsFile).asSequence() + + val allOutcomes = test.outcomeSpec.all + val outcomeStrings = allOutcomes.associateBy { it.toString().trim('(', ')') } + + // get the number of observed outcomes + lines = lines.dropWhile { !it.contains("Observed States") } + val observedOutcomesLine = lines.splitFirst().let { (first, rest) -> lines = rest; first } + val observedSize = Regex("colspan=(\\d+)").find(observedOutcomesLine)!!.groupValues[1].toInt() + + // skip to with outcomes + lines = lines.drop(3) + val linesOutcomes = lines.splitTake(observedSize).let { (first, rest) -> lines = rest; first } + val outcomesOrdered = linesOutcomes.map { + val outcomeString = parseElementData(it) + outcomeStrings[outcomeString] ?: error("unrecognized outcome: $outcomeString") + }.toList() + + // lines with "bgColor" and "width" are the only ones with data + val outcomesCounts = lines.filter { it.contains("bgColor") && it.contains("width") } + .map { parseElementData(it).toLong() } + .chunked(observedSize) + .fold(MutableList(observedSize) { 0L }) { acc, counts -> + acc.also { for (i in acc.indices) acc[i] += counts[i] } + } as List + + val results = List(observedSize) { i -> + val outcome = outcomesOrdered[i] + LitmusOutcomeStats(outcome, outcomesCounts[i], test.outcomeSpec.getType(outcome)) + } + return results + } + + private fun parseElementData(it: String) = it.dropWhile { it != '>' }.dropLastWhile { it != '<' }.trim('>', '<') +} + +// does NOT shadow the common extension function, but can be accessed directly +fun JCStressRunner.runTests( + tests: List>, + params: LitmusRunParams, +): List = startTests(tests, params).invoke() + +/** + * Split a sequence into two: one with the first [size] elements and one with the rest. + */ +fun Sequence.splitTake(size: Int): Pair, Sequence> { + val iter = iterator() + val seq1 = iter.asSequence().take(size) + val seq2 = iter.asSequence() + return seq1 to seq2 +} + +/** + * Split a sequence into its first element and the sequence of rest. + */ +fun Sequence.splitFirst(): Pair> { + val iter = iterator() + return iter.next() to iter.asSequence() +} diff --git a/jcstress/.gitignore b/jcstress/.gitignore new file mode 100644 index 0000000..1110e26 --- /dev/null +++ b/jcstress/.gitignore @@ -0,0 +1,6 @@ +target/ +libs/ +results/ +*.bin.gz +test.iml +generatedSrc/ diff --git a/jcstress/pom.xml b/jcstress/pom.xml new file mode 100644 index 0000000..bbc22ef --- /dev/null +++ b/jcstress/pom.xml @@ -0,0 +1,175 @@ + + + + 4.0.0 + + org.sample + test + 1.0 + jar + + JCStress test sample + + + + + 3.2 + + + + + libs + libs + + true + ignore + + + false + + file://${basedir}/libs + + + + + + org.openjdk.jcstress + jcstress-core + ${jcstress.version} + + + org.jetbrains.kotlin + kotlin-stdlib + 1.9.20 + + + org.jetbrains.litmuskt + litmusktJvm + 1.0 + + + org.jetbrains.litmuskt + litmusktJvmTestsuite + 1.0 + + + + + UTF-8 + + + 0.16 + + + 17 + + + jcstress + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + ${javac.target} + ${javac.target} + ${javac.target} + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + main + package + + shade + + + ${uberjar.name} + + + org.openjdk.jcstress.Main + + + META-INF/TestList + + + + + * + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.0.0 + + + generate-sources + + add-source + + + + generatedSrc/main/ + + + + + + + + + diff --git a/litmus/build.gradle.kts b/litmus/build.gradle.kts deleted file mode 100644 index 2f3ef02..0000000 --- a/litmus/build.gradle.kts +++ /dev/null @@ -1,155 +0,0 @@ -import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi -import org.jetbrains.kotlin.incremental.createDirectory - -plugins { - kotlin("multiplatform") - id("com.google.devtools.ksp") version "1.9.10-1.0.13" -} - -group = "komem.litmus" -version = "1.0-SNAPSHOT" - -@OptIn(ExperimentalKotlinGradlePluginApi::class) -kotlin { - targetHierarchy.default { - common { - withJvm() - withNative() - withLinux() - withMacos() - } - } - - val armEnabled = findProperty("arm") != null - val hostOs = System.getProperty("os.name") -// val isMingwX64 = hostOs.startsWith("Windows") - - val nativeTarget = when { - hostOs == "Mac OS X" -> if (armEnabled) macosArm64() else macosX64() - hostOs == "Linux" -> linuxX64() - else -> throw GradleException("Host OS is not supported") - } - val jvmTarget = jvm { - // executable by default - mainRun { - mainClass.set("JvmMainKt") - } - } - - val affinitySupported = hostOs == "Linux" - nativeTarget.apply { - compilations.getByName("main") { - cinterops { - val barrier by creating { - defFile(project.file("src/nativeInterop/barrier.def")) - headers(project.file("src/nativeInterop/barrier.h")) - } - if (affinitySupported) { - val affinity by creating { - defFile(project.file("src/nativeInterop/kaffinity.def")) - headers(project.file("src/nativeInterop/kaffinity.h")) - } - } - } - if (gradle.startParameter.taskNames.any { it.contains("bitcode") }) { - val tempDir = projectDir.resolve("temp/bitcode") - if (!tempDir.exists()) tempDir.createDirectory() - kotlinOptions.freeCompilerArgs = listOf("-Xtemporary-files-dir=${tempDir.absolutePath}") - } - } - binaries { - executable { - entryPoint = "main" - } - } - } - sourceSets { - val commonMain by getting { - dependencies { - implementation("org.jetbrains.kotlinx:atomicfu:0.20.2") - implementation("com.github.ajalt.clikt:clikt:4.2.1") - } - kotlin.srcDir(buildDir.resolve("generated/ksp/metadata/commonMain/kotlin/")) // ksp - } - val commonTest by getting { - dependencies { - implementation("org.jetbrains.kotlin:kotlin-test:1.9.0") - } - } - - val nativeMain by getting - val nativeTest by getting - - val jvmMain by getting - val jvmTest by getting - - when { - hostOs == "Mac OS X" -> { - val macosMain by getting { - dependsOn(commonMain) - kotlin.srcDirs("src/macosMain/kotlin") - } - } - - hostOs == "Linux" -> { - val linuxMain by getting { - dependsOn(commonMain) - kotlin.srcDirs("src/linuxMain/kotlin") - } - } - } - } -} - -val setupCinterop by tasks.register("setupCinterop") { - group = "interop" - doFirst { - val interopFolder = project.projectDir.resolve("src/nativeInterop") - if (!interopFolder.resolve("kaffinity.def").exists()) { - exec { - executable = interopFolder.resolve("setup.sh").absolutePath - args = listOf(interopFolder.absolutePath) - } - } - } -} - -tasks.matching { it.name.contains("cinterop") && it.name.contains("Linux") } - .forEach { it.dependsOn(setupCinterop) } - -val bitcodeInternal by tasks.register("bitcodeInternal") { - val tempDir = projectDir.resolve("temp/bitcode") - doLast { - exec { - executable = "sh" - args = listOf( - "-c", """ - llvm-dis -o ${tempDir.resolve("bitcode.txt")} ${tempDir.resolve("out.bc")} - """.trimIndent() - ) - } - } -} - -tasks.register("bitcodeDebug") { - dependsOn(tasks.matching { it.name.startsWith("linkDebugExecutable") }) - finalizedBy(bitcodeInternal) -} - -tasks.register("bitcodeRelease") { - dependsOn(tasks.matching { it.name.startsWith("linkReleaseExecutable") }) - finalizedBy(bitcodeInternal) -} - -// ======== ksp ======== - -dependencies { - add("kspCommonMainMetadata", project(":codegen")) -} - -tasks.whenTaskAdded { - if (name == "kspCommonMainKotlinMetadata") { - val kspTask = this - tasks.matching { it.name.startsWith("compileKotlin") }.forEach { it.dependsOn(kspTask) } - } -} diff --git a/litmus/src/commonMain/kotlin/komem/litmus/CliCommon.kt b/litmus/src/commonMain/kotlin/komem/litmus/CliCommon.kt deleted file mode 100644 index d49a8b7..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/CliCommon.kt +++ /dev/null @@ -1,118 +0,0 @@ -package komem.litmus - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.parameters.arguments.* -import com.github.ajalt.clikt.parameters.options.* -import com.github.ajalt.clikt.parameters.types.int -import komem.litmus.barriers.BarrierProducer -import komem.litmus.generated.LitmusTestRegistry -import kotlin.time.Duration - -abstract class CliCommon : CliktCommand( - name = "litmuskt" -) { - private val batchSizeSchedule by option("-b", "--batchSize") - .int().varargValues().default(listOf(1_000_000)) - - private val syncEverySchedule by option("-s", "--syncEvery") - .int().varargValues().default(listOf(100)) - - private val tests by argument("tests") - .multiple(required = true) - .transformAll { args -> - val regexes = args.map { - try { - Regex(it) - } catch (_: IllegalArgumentException) { - fail("invalid regex: $it") - } - } - regexes.flatMap { LitmusTestRegistry[it] }.toSet() - } - .check("no tests were selected") { it.isNotEmpty() } - - private val PARALLELISM_DISABLED = Int.MAX_VALUE - 1 - private val PARALLELISM_AUTO = Int.MAX_VALUE - 2 - private val parallelism by option("-p", "--parallelism") - .int().optionalValue(PARALLELISM_AUTO).default(PARALLELISM_DISABLED) - .check("value must be in range 2..100") { - it in 2..100 || it == PARALLELISM_DISABLED || it == PARALLELISM_AUTO - } - - private val duration by option("-d", "--duration") - .convert { Duration.parse(it) } - .check("value must be positive") { it.isPositive() } - - protected abstract val affinityMapSchedule: List - protected abstract val runner: LitmusRunner - protected abstract val barrierProducer: BarrierProducer - // TODO: we don't talk about memshuffler for now - - override fun run() { - echo("selected tests: \n" + tests.joinToString("\n") { " - " + LitmusTestRegistry.resolveName(it) }) - echo("in total: ${tests.size} tests") - echo() - - val paramsList = variateRunParams( - batchSizeSchedule = batchSizeSchedule, - affinityMapSchedule = affinityMapSchedule, - syncPeriodSchedule = syncEverySchedule, - barrierSchedule = listOf(barrierProducer), - ).toList() - if (paramsList.isEmpty()) { - echo("parameters list is empty; ensure no empty lists are used", err = true) - return - } - echo("parameter combinations per each test: ${paramsList.size}") - echo() - - for (test in tests) { - echo("running test ${LitmusTestRegistry.resolveName(test)}...") - // TODO: handle exceptions - paramsList.map { params -> - // TODO: print ETA (later: calculate based on part of run) - runTest(params, test) - }.mergeResults().let { - echo(it.generateTable()) - } - echo() - } - } - - private fun runTest(params: LitmusRunParams, test: LitmusTest<*>): LitmusResult { - val timeLimit = duration - return when (parallelism) { - PARALLELISM_DISABLED -> { - if (timeLimit == null) { - runner.runTest(params, test) - } else { - runner.runTest(timeLimit, params, test) - } - } - - PARALLELISM_AUTO -> { - if (timeLimit == null) { - runner.runTestParallel(params, test) - } else { - runner.runTestParallel(timeLimit, params, test) - } - } - - else -> { - if (timeLimit == null) { - runner.runTestParallel(parallelism, params, test) - } else { - runner.runTestParallel(parallelism, timeLimit, params, test) - } - } - } - } -} - -fun commonMain(args: Array, cli: CliCommon) { - try { - cli.main(args) - } catch (e: Exception) { - cli.echo(e.stackTraceToString(), err = true, trailingNewline = true) - } -} diff --git a/litmus/src/commonMain/kotlin/komem/litmus/LitmusAutoOutcome.kt b/litmus/src/commonMain/kotlin/komem/litmus/LitmusAutoOutcome.kt deleted file mode 100644 index 5ec8d9d..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/LitmusAutoOutcome.kt +++ /dev/null @@ -1,29 +0,0 @@ -package komem.litmus - -interface LitmusAutoOutcome { - fun getOutcome(): LitmusOutcome -} - -open class LitmusIIOutcome( - var r1: Int = 0, - var r2: Int = 0 -) : LitmusAutoOutcome { - override fun getOutcome() = listOf(r1, r2) -} - -open class LitmusIIIOutcome( - var r1: Int = 0, - var r2: Int = 0, - var r3: Int = 0, -) : LitmusAutoOutcome { - override fun getOutcome() = listOf(r1, r2, r3) -} - -open class LitmusIIIIOutcome( - var r1: Int = 0, - var r2: Int = 0, - var r3: Int = 0, - var r4: Int = 0, -) : LitmusAutoOutcome { - override fun getOutcome() = listOf(r1, r2, r3, r4) -} \ No newline at end of file diff --git a/litmus/src/commonMain/kotlin/komem/litmus/LitmusRunner.kt b/litmus/src/commonMain/kotlin/komem/litmus/LitmusRunner.kt deleted file mode 100644 index 8778061..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/LitmusRunner.kt +++ /dev/null @@ -1,85 +0,0 @@ -package komem.litmus - -import kotlin.time.Duration -import kotlin.time.TimeSource - -abstract class LitmusRunner { - abstract fun runTest(params: LitmusRunParams, test: LitmusTest): LitmusResult - - // be extremely careful due to LTOutcome = Any? - protected fun List.calcStats(outcomeSpec: LitmusOutcomeSpec): LitmusResult = this - .groupingBy { it } - .eachCount() - .map { (outcome, count) -> - LitmusOutcomeStats(outcome, count.toLong(), outcomeSpec.getType(outcome)) - } -} - -fun LitmusRunner.runTest( - timeLimit: Duration, - params: LitmusRunParams, - test: LitmusTest, -): LitmusResult { - val results = mutableListOf() - val start = TimeSource.Monotonic.markNow() - while (start.elapsedNow() < timeLimit) { - results += runTest(params, test) - } - return results.mergeResults() -} - -/* - * Note: interprets AffinityMap as a sequence of smaller maps - * Example: for a map [ [0], [1], [2], [3] ],a test with 2 threads, and 2 instances, the - * first instance will have a [ [0], [1] ] map and the second one will have [ [2], [3] ]. - */ -fun LitmusRunner.runTestParallel( - instances: Int, - params: LitmusRunParams, - test: LitmusTest, -): LitmusResult { - val allOutcomes = List(instances) { instanceIndex -> - val newAffinityMap = params.affinityMap?.let { oldMap -> - AffinityMap { threadIndex -> - oldMap.allowedCores(instanceIndex * test.threadCount + threadIndex) - } - } - val newParams = params.copy(affinityMap = newAffinityMap) - runTest(newParams, test) - } - return allOutcomes.mergeResults() -} - -fun LitmusRunner.runTestParallel( - params: LitmusRunParams, - test: LitmusTest -): LitmusResult = runTestParallel( - cpuCount() / test.threadCount, - params, - test -) - -fun LitmusRunner.runTestParallel( - instances: Int, - timeLimit: Duration, - params: LitmusRunParams, - test: LitmusTest, -): LitmusResult { - val results = mutableListOf() - val start = TimeSource.Monotonic.markNow() - while (start.elapsedNow() < timeLimit) { - results += runTestParallel(instances, params, test) - } - return results.mergeResults() -} - -fun LitmusRunner.runTestParallel( - timeLimit: Duration, - params: LitmusRunParams, - test: LitmusTest, -): LitmusResult = runTestParallel( - cpuCount() / test.threadCount, - timeLimit, - params, - test -) diff --git a/litmus/src/commonMain/kotlin/komem/litmus/testsuite/ClassicTests.kt b/litmus/src/commonMain/kotlin/komem/litmus/testsuite/ClassicTests.kt deleted file mode 100644 index b22f51f..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/testsuite/ClassicTests.kt +++ /dev/null @@ -1,382 +0,0 @@ -package komem.litmus.testsuite - -import komem.litmus.* -import kotlin.concurrent.Volatile - -data class IntHolder(val x: Int) - -class IntHolderCtor { - val x = 1 -} - -val ATOM: LitmusTest<*> = litmusTest({ - object { - var x = 0 - var o = 0 - } -}) { - thread { - x = -1 // signed 0xFFFFFFFF - } - thread { - o = x - } - outcome { - o - } - spec { - accept(0) - accept(-1) - } -} - -val SB: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - var x = 0 - var y = 0 - } -}) { - thread { - x = 1 - r1 = y - } - thread { - y = 1 - r2 = x - } - // no need for explicit outcome{} - spec { - accept(0, 1) - accept(1, 0) - accept(1, 1) - interesting(0, 0) - } -} - -val SBVolatile: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - @Volatile - var x = 0 - - @Volatile - var y = 0 - } -}) { - thread { - x = 1 - r1 = y - } - thread { - y = 1 - r2 = x - } - // no need for explicit outcome{} - spec { - accept(0, 1) - accept(1, 0) - accept(1, 1) - } -} - -val MP: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - var x = 0 - var y = 0 - } -}) { - thread { - x = 1 - y = 1 - } - thread { - r1 = y - r2 = x - } - spec { - accept(0, 0) - accept(0, 1) - accept(1, 1) - interesting(1, 0) - } -} - -val MPVolatile: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - @Volatile - var x = 0 - - @Volatile - var y = 0 - } -}) { - thread { - x = 1 - y = 1 - } - thread { - r1 = y - r2 = x - } - spec { - accept(0, 0) - accept(0, 1) - accept(1, 1) - } -} - -val MP_DRF: LitmusTest<*> = litmusTest({ - object { - var x = 0 - - @Volatile - var y = 0 - var o = 0 - } -}) { - thread { - x = 1 - y = 1 - } - thread { - o = if (y != 0) x else -1 - } - outcome { - o - } - spec { - accept(1) - accept(-1) - } -} - -val CoRR: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - var x = 0 - } -}) { - thread { - x = 1 - } - thread { - r1 = x - r2 = x - } - spec { - accept(0, 0) - accept(0, 1) - accept(1, 1) - interesting(1, 0) - } -} - -val CoRR_CSE: LitmusTest<*> = litmusTest({ - data class Holder(var x: Int) - object : LitmusIIIOutcome() { - val holder1 = Holder(0) - val holder2 = holder1 - } -}) { - thread { - holder1.x = 1 - } - thread { - val h1 = holder1 - val h2 = holder2 - r1 = h1.x - r2 = h2.x - r3 = h1.x - } - spec { - interesting(1, 0, 0) - interesting(1, 1, 0) - default(LitmusOutcomeType.ACCEPTED) - } -} - -val IRIW: LitmusTest<*> = litmusTest({ - object : LitmusIIIIOutcome() { - var x = 0 - var y = 0 - } -}) { - thread { - x = 1 - } - thread { - y = 1 - } - thread { - r1 = x - r2 = y - } - thread { - r3 = y - r4 = x - } - spec { - interesting(1, 0, 1, 0) - interesting(0, 1, 0, 1) - default(LitmusOutcomeType.ACCEPTED) - } -} - -val IRIWVolatile: LitmusTest<*> = litmusTest({ - object : LitmusIIIIOutcome() { - @Volatile - var x = 0 - - @Volatile - var y = 0 - } -}) { - thread { - x = 1 - } - thread { - y = 1 - } - thread { - r1 = x - r2 = y - } - thread { - r3 = y - r4 = x - } - spec { - forbid(1, 0, 1, 0) - default(LitmusOutcomeType.ACCEPTED) - } -} - -val UPUB: LitmusTest<*> = litmusTest({ - object { - var h: IntHolder? = null - var o = 0 - } -}) { - thread { - h = IntHolder(0) - } - thread { - o = h?.x ?: -1 - } - outcome { - o - } - spec { - accept(0) - accept(-1) - } -} - -val UPUBCtor: LitmusTest<*> = litmusTest({ - object { - var h: IntHolderCtor? = null - var o = 0 - } -}) { - thread { - h = IntHolderCtor() - } - thread { - o = h?.x ?: -1 - } - outcome { - o - } - spec { - accept(1) - accept(-1) - } -} - -val LB_DEPS_OOTA: LitmusTest<*> = litmusTest({ - object { - var x = 0 - var y = 0 - var a = 0 - var b = 0 - } -}) { - thread { - a = x - y = a - } - thread { - b = y - x = b - } - outcome { - listOf(a, b) - } - spec { - accept(0, 0) - } -} - -val LB: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - var x = 0 - var y = 0 - } -}) { - thread { - r1 = x - y = 1 - } - thread { - r2 = y - x = 1 - } - spec { - accept(0, 0) - accept(1, 0) - accept(0, 1) - interesting(1, 1) - } -} - -val LBVolatile: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - @Volatile - var x = 0 - - @Volatile - var y = 0 - } -}) { - thread { - r1 = x - y = 1 - } - thread { - r2 = y - x = 1 - } - spec { - accept(0, 0) - accept(1, 0) - accept(0, 1) - } -} - -val LBFakeDEPS: LitmusTest<*> = litmusTest({ - object : LitmusIIOutcome() { - var x = 0 - var y = 0 - } -}) { - thread { - r1 = x - y = 1 + r1 * 0 - } - thread { - r2 = y - x = r2 - } - spec { - accept(0, 0) - accept(0, 1) - } -} diff --git a/litmus/src/commonMain/kotlin/komem/litmus/testsuite/CustomTests.kt b/litmus/src/commonMain/kotlin/komem/litmus/testsuite/CustomTests.kt deleted file mode 100644 index 9f00dfe..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/testsuite/CustomTests.kt +++ /dev/null @@ -1,26 +0,0 @@ -package komem.litmus.testsuite - -import komem.litmus.LitmusTest -import komem.litmus.litmusTest - -val MPNoDRF: LitmusTest<*> = litmusTest({ - object { - var x = 0 - var y = 0 - var o = 0 - } -}) { - thread { - x = 1 - y = 1 - } - thread { - o = if (y != 0) x else -1 - } - outcome { o } - spec { - accept(1) - accept(-1) - interesting(0) - } -} diff --git a/litmus/src/commonMain/kotlin/komem/litmus/testsuite/UPUBExtraTests.kt b/litmus/src/commonMain/kotlin/komem/litmus/testsuite/UPUBExtraTests.kt deleted file mode 100644 index c63318b..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/testsuite/UPUBExtraTests.kt +++ /dev/null @@ -1,101 +0,0 @@ -package komem.litmus.testsuite - -import komem.litmus.LitmusTest -import komem.litmus.litmusTest -import kotlin.concurrent.Volatile - -val UPUBVolatile: LitmusTest<*> = litmusTest({ - object { - @Volatile - var h: IntHolder? = null - var o = 0 - } -}) { - thread { - h = IntHolder(0) - } - thread { - o = h?.x ?: -1 - } - outcome { o } - spec { - accept(0) - accept(-1) - } -} - -val UPUBArray: LitmusTest<*> = litmusTest({ - object { - var arr: Array? = null - var o = 0 - } -}) { - thread { - arr = Array(10) { 0 } - } - thread { - o = arr?.get(0) ?: -1 - } - outcome { o } - spec { - accept(0) - accept(-1) - } -} - -private class UPUBRefInner(val x: Int) -private class UPUBRefHolder(val ref: UPUBRefInner) - -val UPUBRef: LitmusTest<*> = litmusTest({ - object { - var h: UPUBRefHolder? = null - var o = 0 - } -}) { - thread { - val ref = UPUBRefInner(1) - h = UPUBRefHolder(ref) - } - thread { - val t = h - o = t?.ref?.x ?: -1 - } - outcome { o } - spec { - accept(1) - accept(-1) - } -} - -private class UPUBIntHolderInnerLeaking { - var ih: InnerHolder? = null - - inner class InnerHolder { - val x: Int - - init { - x = 1 - ih = this - } - } -} - -val UBUBCtorLeaking: LitmusTest<*> = litmusTest({ - object { - var h = UPUBIntHolderInnerLeaking() - var o = 0 - } -}) { - thread { - h.InnerHolder() - } - thread { - o = h.ih?.x ?: -1 - } - outcome { o } - spec { - accept(1) - accept(0) - accept(-1) - } -} diff --git a/litmus/src/commonTest/kotlin/komem.litmus/infra/TestDefaults.kt b/litmus/src/commonTest/kotlin/komem.litmus/infra/TestDefaults.kt deleted file mode 100644 index e78d528..0000000 --- a/litmus/src/commonTest/kotlin/komem.litmus/infra/TestDefaults.kt +++ /dev/null @@ -1,21 +0,0 @@ -package komem.litmus.infra - -import komem.litmus.LitmusOutcomeType -import komem.litmus.LitmusRunParams -import komem.litmus.LitmusRunner -import komem.litmus.LitmusTest -import kotlin.test.assertTrue - -expect val defaultParams: LitmusRunParams -expect val defaultRunner: LitmusRunner - -fun LitmusTest<*>.run( - params: LitmusRunParams = defaultParams, - runner: LitmusRunner = defaultRunner, -) { - val results = runner.runTest(params, this) - assertTrue { results.none { it.type == LitmusOutcomeType.FORBIDDEN } } - if (results.any { it.type == LitmusOutcomeType.INTERESTING }) { - println("interesting cases detected") // TODO: provide test name (?) - } -} diff --git a/litmus/src/commonTest/kotlin/komem.litmus/testsuite/ClassicTests.kt b/litmus/src/commonTest/kotlin/komem.litmus/testsuite/ClassicTests.kt deleted file mode 100644 index 9ecb1d0..0000000 --- a/litmus/src/commonTest/kotlin/komem.litmus/testsuite/ClassicTests.kt +++ /dev/null @@ -1,52 +0,0 @@ -package komem.litmus.testsuite - -import komem.litmus.infra.run -import kotlin.test.Test - -class ClassicTests { - - @Test - fun atom() = ATOM.run() - - @Test - fun sb() = SB.run() - - @Test - fun sbVolatile() = SBVolatile.run() - - @Test - fun mp() = MP.run() - - @Test - fun mpVolatile() = MPVolatile.run() - - @Test - fun mpDrf() = MP_DRF.run() - - @Test - fun coRR() = CoRR.run() - - @Test - fun coRRCse() = CoRR_CSE.run() - - @Test - fun iriw() = IRIW.run() - - @Test - fun iriwVolatile() = IRIWVolatile.run() - - @Test - fun upub() = UPUB.run() - - @Test - fun upubCtor() = UPUBCtor.run() - - @Test - fun lbDepsOOTA() = LB_DEPS_OOTA.run() - - @Test - fun lb() = LB.run() - - @Test - fun lbVolatile() = LBVolatile.run() -} diff --git a/litmus/src/commonTest/kotlin/komem.litmus/testsuite/CustomTests.kt b/litmus/src/commonTest/kotlin/komem.litmus/testsuite/CustomTests.kt deleted file mode 100644 index 489cacb..0000000 --- a/litmus/src/commonTest/kotlin/komem.litmus/testsuite/CustomTests.kt +++ /dev/null @@ -1,9 +0,0 @@ -package komem.litmus.testsuite - -import komem.litmus.infra.run -import kotlin.test.Test - -class CustomTests { - @Test - fun mpNoDrf() = MPNoDRF.run() -} diff --git a/litmus/src/commonTest/kotlin/komem.litmus/testsuite/UPUBExtraTests.kt b/litmus/src/commonTest/kotlin/komem.litmus/testsuite/UPUBExtraTests.kt deleted file mode 100644 index 2a9e7a2..0000000 --- a/litmus/src/commonTest/kotlin/komem.litmus/testsuite/UPUBExtraTests.kt +++ /dev/null @@ -1,18 +0,0 @@ -package komem.litmus.testsuite - -import komem.litmus.infra.run -import kotlin.test.Test - -class UPUBExtraTests { - @Test - fun upubVolatile() = UPUBVolatile.run() - - @Test - fun upubArray() = UPUBArray.run() - - @Test - fun upubRef() = UPUBRef.run() - - @Test - fun upubCtorLeaking() = UBUBCtorLeaking.run() -} diff --git a/litmus/src/jvmMain/kotlin/JvmMain.kt b/litmus/src/jvmMain/kotlin/JvmMain.kt deleted file mode 100644 index 3c1b685..0000000 --- a/litmus/src/jvmMain/kotlin/JvmMain.kt +++ /dev/null @@ -1,4 +0,0 @@ -import komem.litmus.CliJvm -import komem.litmus.commonMain - -fun main(args: Array) = commonMain(args, CliJvm()) diff --git a/litmus/src/jvmMain/kotlin/komem/litmus/CliJvm.kt b/litmus/src/jvmMain/kotlin/komem/litmus/CliJvm.kt deleted file mode 100644 index 056c2e7..0000000 --- a/litmus/src/jvmMain/kotlin/komem/litmus/CliJvm.kt +++ /dev/null @@ -1,9 +0,0 @@ -package komem.litmus - -import komem.litmus.barriers.JvmSpinBarrier - -class CliJvm : CliCommon() { - override val runner = JvmThreadRunner - override val barrierProducer = ::JvmSpinBarrier - override val affinityMapSchedule = listOf(null) -} diff --git a/litmus/src/jvmMain/kotlin/komem/litmus/JvmThreadRunner.kt b/litmus/src/jvmMain/kotlin/komem/litmus/JvmThreadRunner.kt deleted file mode 100644 index 3188f4b..0000000 --- a/litmus/src/jvmMain/kotlin/komem/litmus/JvmThreadRunner.kt +++ /dev/null @@ -1,29 +0,0 @@ -package komem.litmus - -// does not support affinity -object JvmThreadRunner : LitmusRunner() { - - override fun runTest(params: LitmusRunParams, test: LitmusTest): LitmusResult { - - val states = List(params.batchSize) { test.stateProducer() } - val barrier = params.barrierProducer(test.threadCount) - val outcomeFinalizer = test.outcomeFinalizer - - val threads = List(test.threadCount) { threadIndex -> - Thread { - val threadFunction = test.threadFunctions[threadIndex] - val syncPeriod = params.syncPeriod - for (i in states.indices) { - if (i % syncPeriod == 0) barrier.await() - states[i].threadFunction() - } - } - } - threads.forEach { it.start() } - threads.forEach { it.join() } // await all threads - - val outcomes = states.map { it.outcomeFinalizer() } - assert(outcomes.size == params.batchSize) - return outcomes.calcStats(test.outcomeSpec) - } -} diff --git a/litmus/src/jvmTest/kotlin/komem/litmus/infra/TestDefaults.jvm.kt b/litmus/src/jvmTest/kotlin/komem/litmus/infra/TestDefaults.jvm.kt deleted file mode 100644 index 6c64a3a..0000000 --- a/litmus/src/jvmTest/kotlin/komem/litmus/infra/TestDefaults.jvm.kt +++ /dev/null @@ -1,14 +0,0 @@ -package komem.litmus.infra - -import komem.litmus.JvmThreadRunner -import komem.litmus.LitmusRunParams -import komem.litmus.LitmusRunner -import komem.litmus.barriers.JvmSpinBarrier - -actual val defaultRunner: LitmusRunner = JvmThreadRunner -actual val defaultParams: LitmusRunParams = LitmusRunParams( - batchSize = 1_000_000, - syncPeriod = 1000, - affinityMap = null, - barrierProducer = ::JvmSpinBarrier -) diff --git a/litmus/src/linuxMain/kotlin/komem/litmus/AffinityBindingsImplPosix.kt b/litmus/src/linuxMain/kotlin/komem/litmus/AffinityBindingsImplPosix.kt deleted file mode 100644 index 1be1e48..0000000 --- a/litmus/src/linuxMain/kotlin/komem/litmus/AffinityBindingsImplPosix.kt +++ /dev/null @@ -1,47 +0,0 @@ -@file:OptIn(kotlin.native.concurrent.ObsoleteWorkersApi::class) - -package komem.litmus - -import kaffinity.* -import kotlinx.cinterop.* -import platform.posix.cpu_set_t -import platform.posix.errno -import platform.posix.pthread_t -import platform.posix.strerror -import kotlin.native.concurrent.Worker - -@OptIn(ExperimentalForeignApi::class) -private fun Int.callCheck() { - if (this != 0) { - val err = strerror(errno)!!.toKString() - throw IllegalStateException("C call error: $err") - } -} - -@OptIn(ExperimentalForeignApi::class) -private fun setAffinity(thread: pthread_t, cpus: Set): Unit = memScoped { - require(cpus.isNotEmpty()) - val set = alloc() - for (cpu in cpus) cpu_set(cpu, set.ptr) - set_affinity(thread, set.ptr).callCheck() -} - -@OptIn(ExperimentalForeignApi::class) -private fun getAffinity(thread: pthread_t): Set = memScoped { - val set = alloc() - get_affinity(thread, set.ptr).callCheck() - return (0..) { - setAffinity(w.platformThreadId, cpus) - } - - override fun getAffinity(w: Worker): Set { - return getAffinity(w.platformThreadId) - } -} diff --git a/litmus/src/macosMain/kotlin/komem/litmus/AffinityBindingsImplNoop.kt b/litmus/src/macosMain/kotlin/komem/litmus/AffinityBindingsImplNoop.kt deleted file mode 100644 index f74d3b6..0000000 --- a/litmus/src/macosMain/kotlin/komem/litmus/AffinityBindingsImplNoop.kt +++ /dev/null @@ -1,3 +0,0 @@ -package komem.litmus - -actual fun getAffinityManager(): AffinityManager? = null \ No newline at end of file diff --git a/litmus/src/nativeInterop/kaffinity.def b/litmus/src/nativeInterop/kaffinity.def new file mode 100644 index 0000000..7666005 --- /dev/null +++ b/litmus/src/nativeInterop/kaffinity.def @@ -0,0 +1 @@ +linkerOpts.linux = /home/denis/prog/komem-litmus/litmus/src/nativeInterop/kaffinity_gnu.o diff --git a/litmus/src/nativeInterop/kaffinity.h b/litmus/src/nativeInterop/kaffinity.h deleted file mode 100644 index 5d31bda..0000000 --- a/litmus/src/nativeInterop/kaffinity.h +++ /dev/null @@ -1,9 +0,0 @@ -#include - -int set_affinity(pthread_t thread, cpu_set_t* set); -int get_affinity(pthread_t thread, cpu_set_t* set); - -void cpu_zero(cpu_set_t* set); -void cpu_set(int cpu, cpu_set_t* set); -int cpu_isset(int cpu, cpu_set_t* set); -int cpu_setsize(); diff --git a/litmus/src/nativeInterop/kaffinity_gnu.o b/litmus/src/nativeInterop/kaffinity_gnu.o new file mode 100644 index 0000000000000000000000000000000000000000..8687434e67b9a1784a46a21623e812a52bca7b3f GIT binary patch literal 2248 zcmbuA-%C?b9KgSKnwkA@MGt}@dnpq0vPr!}%2(^ZEWb=YG%G-R;1s z4wt5Z5)F>S`b<%Pi7o54-!lEM6L!Fl+J;|2AwP@sid)FBypX9?kqP1md=yGa@}xEo5R7~4AoY%7HPkH$!yIdi&LVgr- z^@n&e)m>ayj;-MCa+$!2xG!WNFYb)x*D3(6w~0(wj+PPGz6kjfSx#hg6+)XA+ujNJT}b!8EYoBqs%5x9#!U;SbFH}Ng|9-+r*oRP zJ8i4?6ex5N+@^7-guf%`3=zqZeGbR4cqW?MjYwiAy!v5H`LD2zL!nd^z4g?itZv1ZK zdu-f#S7Z%1sIN{rf`H;xoYHN`%f(;d_;Xyy>J{1%=8u?LJwp2$^BVqfTD?JLGQY^& z>IpI!2>13zV^MP?)E7(XJ&-iakU|hQ`x8d^Y6ziXp;#RH%EchG$MnQgp;03-3}j%w zrK3q@p-x6e4cJe6K%qNFlFuAeH?di)yIbp zuKGCb;Hr>qZv-v*ErJ8D=Xj-Dyr}}nL z>nnW}oyxYyeJBUZ#GXX96$jl@yDW0P#}3g}{ZlxC28BBF^PIn^0$DrryOEbWzX?9S zDbAzwqg<7*&ad3slrqm7s^)!cM@DDl%-_ZwRf#O}fsU8ZjlHJtt@G?DrH=l "$DEF_FILE_PATH" diff --git a/litmus/src/nativeMain/kotlin/NativeMain.kt b/litmus/src/nativeMain/kotlin/NativeMain.kt deleted file mode 100755 index 42e5e4b..0000000 --- a/litmus/src/nativeMain/kotlin/NativeMain.kt +++ /dev/null @@ -1,4 +0,0 @@ -import komem.litmus.CliNative -import komem.litmus.commonMain - -fun main(args: Array) = commonMain(args, CliNative()) diff --git a/litmus/src/nativeMain/kotlin/komem.litmus/NativeUtils.kt b/litmus/src/nativeMain/kotlin/komem.litmus/NativeUtils.kt deleted file mode 100644 index c2d2f36..0000000 --- a/litmus/src/nativeMain/kotlin/komem.litmus/NativeUtils.kt +++ /dev/null @@ -1,6 +0,0 @@ -package komem.litmus - -import platform.posix._SC_NPROCESSORS_ONLN -import platform.posix.sysconf - -actual fun cpuCount(): Int = sysconf(_SC_NPROCESSORS_ONLN).toInt() diff --git a/litmus/src/nativeTest/kotlin/komem/litmus/NativeTest.kt b/litmus/src/nativeTest/kotlin/komem/litmus/NativeTest.kt deleted file mode 100644 index afed7f8..0000000 --- a/litmus/src/nativeTest/kotlin/komem/litmus/NativeTest.kt +++ /dev/null @@ -1,15 +0,0 @@ -package komem.litmus - -import komem.litmus.barriers.CinteropSpinBarrier -import komem.litmus.testsuite.ATOM -import kotlin.test.Test - -class NativeTest { - @Test - fun hehe() { - val test = ATOM - val runner = WorkerRunner - val params = LitmusRunParams(1_000_000, 100, null, ::CinteropSpinBarrier) - runner.runTest(params, test).prettyPrint() - } -} diff --git a/litmus/src/nativeTest/kotlin/komem/litmus/infra/TestDefaults.native.kt b/litmus/src/nativeTest/kotlin/komem/litmus/infra/TestDefaults.native.kt deleted file mode 100644 index 06546c4..0000000 --- a/litmus/src/nativeTest/kotlin/komem/litmus/infra/TestDefaults.native.kt +++ /dev/null @@ -1,14 +0,0 @@ -package komem.litmus.infra - -import komem.litmus.LitmusRunParams -import komem.litmus.LitmusRunner -import komem.litmus.WorkerRunner -import komem.litmus.barriers.CinteropSpinBarrier - -actual val defaultRunner: LitmusRunner = WorkerRunner -actual val defaultParams: LitmusRunParams = LitmusRunParams( - batchSize = 1_000_000, - syncPeriod = 10, - affinityMap = null, - barrierProducer = ::CinteropSpinBarrier -) diff --git a/settings.gradle.kts b/settings.gradle.kts index 81a8a73..ef43551 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,7 @@ rootProject.name = "litmuskt" -include(":litmus") +include(":core") include(":codegen") +include(":jcstress-wrapper") +include(":cli") +include(":testsuite") diff --git a/testsuite/build.gradle.kts b/testsuite/build.gradle.kts new file mode 100644 index 0000000..7f64f52 --- /dev/null +++ b/testsuite/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + kotlin("multiplatform") + id("com.google.devtools.ksp") version "1.9.20-1.0.13" + `java-library` +} + +kotlin { + // targets have to be the same as in :cli (because it depends on this subproject) + linuxX64() +// linuxArm64() + macosX64() + macosArm64() + jvm { + withJava() + } + + sourceSets { + commonMain { + dependencies { + implementation(project(":core")) + } + } + } +} + +// ======== ksp ======== + +val kspTasks = setOf("kspJvm", "kspLinuxX64", "kspMacosX64", "kspMacosArm64") + +dependencies { + for (kspTask in kspTasks) { + add(kspTask, project(":codegen")) + } +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestContainer.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestContainer.kt new file mode 100644 index 0000000..5cb55b4 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestContainer.kt @@ -0,0 +1,4 @@ +package org.jetbrains.litmuskt + +@Target(AnnotationTarget.CLASS) +annotation class LitmusTestContainer diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestExtensions.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestExtensions.kt new file mode 100644 index 0000000..0e78d6e --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTestExtensions.kt @@ -0,0 +1,14 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.generated.LitmusTestRegistry + +val LitmusTest<*>.alias get() = LitmusTestRegistry.getAlias(this) +val LitmusTest<*>.qualifiedName get() = LitmusTestRegistry.getFQN(this) + +val LitmusTest<*>.javaClassName get() = alias.replace('.', '_') +val LitmusTest<*>.javaFQN + get(): String { + val kotlinQN = qualifiedName + val lastDotIdx = kotlinQN.indexOfLast { it == '.' } + return kotlinQN.replaceRange(lastDotIdx..lastDotIdx, "_") + } diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/generated/LitmusTestRegistry.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/generated/LitmusTestRegistry.kt new file mode 100644 index 0000000..79e7202 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/generated/LitmusTestRegistry.kt @@ -0,0 +1,10 @@ +package org.jetbrains.litmuskt.generated + +import org.jetbrains.litmuskt.LitmusTest + +expect object LitmusTestRegistry { + operator fun get(regex: Regex): List> + fun all(): List> + fun getAlias(test: LitmusTest<*>): String + fun getFQN(test: LitmusTest<*>): String +} \ No newline at end of file diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/ArrayVolatile.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/ArrayVolatile.kt new file mode 100644 index 0000000..f62d2a4 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/ArrayVolatile.kt @@ -0,0 +1,36 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIIOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.autooutcomes.interesting +import org.jetbrains.litmuskt.litmusTest +import kotlin.concurrent.Volatile + +// source: https://github.com/openjdk/jcstress/blob/master/jcstress-samples/src/main/java/org/openjdk/jcstress/samples/jmm/advanced/AdvancedJMM_08_ArrayVolatility.java +@LitmusTestContainer +object ArrayVolatile { + + val Array = litmusTest({ + object : LitmusIIOutcome() { + // @Volatile cannot be put on `val`-s + @Volatile + var arr = IntArray(2) + } + }) { + thread { + arr[0] = 1 + arr[1] = 1 + } + thread { + r1 = arr[1] + r2 = arr[0] + } + spec { + accept(0, 0) + accept(1, 1) + accept(0, 1) + interesting(1, 0) + } + } +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Atomicity.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Atomicity.kt new file mode 100644 index 0000000..6b832e5 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Atomicity.kt @@ -0,0 +1,66 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusOutcomeType +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIOutcome +import org.jetbrains.litmuskt.autooutcomes.LitmusLOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.litmusTest +import kotlin.concurrent.Volatile + +@LitmusTestContainer +object Atomicity { + + val Int = litmusTest({ + object : LitmusIOutcome() { + var x = 0 + } + }) { + thread { + x = -1 // signed 0xFFFFFFFF + } + thread { + r1 = x + } + spec { + accept(0) + accept(-1) + } + } + + val Long = litmusTest({ + object : LitmusLOutcome() { + var x = 0L + } + }) { + thread { + x = -1 // signed 0xFFFFFFFF_FFFFFFFF + } + thread { + r1 = x + } + spec { + accept(0) + accept(-1) + default(LitmusOutcomeType.INTERESTING) + } + } + + val LongVolatile = litmusTest({ + object : LitmusLOutcome() { + @Volatile + var x = 0L + } + }) { + thread { + x = -1 // signed 0xFFFFFFFF_FFFFFFFF + } + thread { + r1 = x + } + spec { + accept(0) + accept(-1) + } + } +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Coherence.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Coherence.kt new file mode 100644 index 0000000..d666663 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Coherence.kt @@ -0,0 +1,58 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusOutcomeType +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIIIOutcome +import org.jetbrains.litmuskt.autooutcomes.LitmusIIOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.autooutcomes.interesting +import org.jetbrains.litmuskt.litmusTest + +@LitmusTestContainer +object Coherence { + + val Plain = litmusTest({ + object : LitmusIIOutcome() { + var x = 0 + } + }) { + thread { + x = 1 + } + thread { + r1 = x + r2 = x + } + spec { + accept(0, 0) + accept(0, 1) + accept(1, 1) + interesting(1, 0) + } + } + + val CSE = litmusTest({ + data class Holder(var x: Int) + object : LitmusIIIOutcome() { + val holder1 = Holder(0) + val holder2 = holder1 + } + }) { + thread { + holder1.x = 1 + } + thread { + val h1 = holder1 + val h2 = holder2 + r1 = h1.x + r2 = h2.x + r3 = h1.x + } + spec { + interesting(1, 0, 0) + interesting(1, 1, 0) + default(LitmusOutcomeType.ACCEPTED) + } + } + +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/IndependentReadsOfIndependentWrites.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/IndependentReadsOfIndependentWrites.kt new file mode 100644 index 0000000..b9e2a05 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/IndependentReadsOfIndependentWrites.kt @@ -0,0 +1,70 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusOutcomeType +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIIIIOutcome +import org.jetbrains.litmuskt.autooutcomes.forbid +import org.jetbrains.litmuskt.autooutcomes.interesting +import org.jetbrains.litmuskt.litmusTest +import kotlin.concurrent.Volatile + +@LitmusTestContainer +object IndependentReadsOfIndependentWrites { + + val Plain = litmusTest({ + object : LitmusIIIIOutcome() { + var x = 0 + var y = 0 + } + }) { + thread { + x = 1 + } + thread { + y = 1 + } + thread { + r1 = x + r2 = y + } + thread { + r3 = y + r4 = x + } + spec { + interesting(1, 0, 1, 0) + default(LitmusOutcomeType.ACCEPTED) + } + } + + // because of Java, tests cannot be named "Volatile" + val VolatileAnnotated = litmusTest({ + object : LitmusIIIIOutcome() { + @Volatile + var x = 0 + + @Volatile + var y = 0 + } + }) { + thread { + x = 1 + } + thread { + y = 1 + } + thread { + r1 = x + r2 = y + } + thread { + r3 = y + r4 = x + } + spec { + forbid(1, 0, 1, 0) + default(LitmusOutcomeType.ACCEPTED) + } + } + +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/LoadBuffering.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/LoadBuffering.kt new file mode 100644 index 0000000..f0edd53 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/LoadBuffering.kt @@ -0,0 +1,98 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIIOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.autooutcomes.interesting +import org.jetbrains.litmuskt.litmusTest +import kotlin.concurrent.Volatile + +@LitmusTestContainer +object LoadBuffering { + + val NoOutOfThinAirValues = litmusTest({ + object : LitmusIIOutcome() { + var x = 0 + var y = 0 + } + }) { + thread { + r1 = x + y = r1 + } + thread { + r2 = y + x = r2 + } + spec { + accept(0, 0) + } + } + + val Plain = litmusTest({ + object : LitmusIIOutcome() { + var x = 0 + var y = 0 + } + }) { + thread { + r1 = x + y = 1 + } + thread { + r2 = y + x = 1 + } + spec { + accept(0, 0) + accept(1, 0) + accept(0, 1) + interesting(1, 1) + } + } + + val VolatileAnnotated = litmusTest({ + object : LitmusIIOutcome() { + @Volatile + var x = 0 + + @Volatile + var y = 0 + } + }) { + thread { + r1 = x + y = 1 + } + thread { + r2 = y + x = 1 + } + spec { + accept(0, 0) + accept(1, 0) + accept(0, 1) + } + } + + val PlainWithFakeDependencies = litmusTest({ + object : LitmusIIOutcome() { + var x = 0 + var y = 0 + } + }) { + thread { + r1 = x + y = 1 + r1 * 0 + } + thread { + r2 = y + x = r2 + } + spec { + accept(0, 0) + accept(0, 1) + } + } + +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/MessagePassing.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/MessagePassing.kt new file mode 100644 index 0000000..4bee96d --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/MessagePassing.kt @@ -0,0 +1,101 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIIOutcome +import org.jetbrains.litmuskt.autooutcomes.LitmusIOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.autooutcomes.interesting +import org.jetbrains.litmuskt.litmusTest +import kotlin.concurrent.Volatile + +@LitmusTestContainer +object MessagePassing { + + val Plain = litmusTest({ + object : LitmusIIOutcome() { + var x = 0 + var y = 0 + } + }) { + thread { + x = 1 + y = 1 + } + thread { + r1 = y + r2 = x + } + spec { + accept(0, 0) + accept(0, 1) + accept(1, 1) + interesting(1, 0) + } + } + + val VolatileAnnotated = litmusTest({ + object : LitmusIIOutcome() { + @Volatile + var x = 0 + + @Volatile + var y = 0 + } + }) { + thread { + x = 1 + y = 1 + } + thread { + r1 = y + r2 = x + } + spec { + accept(0, 0) + accept(0, 1) + accept(1, 1) + } + } + + val RaceFree = litmusTest({ + object : LitmusIOutcome() { + var x = 0 + + @Volatile + var y = 0 + } + }) { + thread { + x = 1 + y = 1 + } + thread { + r1 = if (y != 0) x else -1 + } + spec { + accept(1) + accept(-1) + } + } + + val MissingVolatile = litmusTest({ + object : LitmusIOutcome() { + var x = 0 + var y = 0 + } + }) { + thread { + x = 1 + y = 1 + } + thread { + r1 = if (y != 0) x else -1 + } + spec { + accept(1) + accept(-1) + interesting(0) + } + } + +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/StoreBuffering.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/StoreBuffering.kt new file mode 100644 index 0000000..e87584e --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/StoreBuffering.kt @@ -0,0 +1,62 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIIOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.autooutcomes.forbid +import org.jetbrains.litmuskt.autooutcomes.interesting +import org.jetbrains.litmuskt.litmusTest +import kotlin.concurrent.Volatile + +@LitmusTestContainer +object StoreBuffering { + + val Plain = litmusTest({ + object : LitmusIIOutcome() { + var x = 0 + var y = 0 + } + }) { + thread { + x = 1 + r1 = y + } + thread { + y = 1 + r2 = x + } + // no need for explicit outcome{} + spec { + accept(0, 1) + accept(1, 0) + accept(1, 1) + interesting(0, 0) + } + } + + val VolatileAnnotated = litmusTest({ + object : LitmusIIOutcome() { + @Volatile + var x = 0 + + @Volatile + var y = 0 + } + }) { + thread { + x = 1 + r1 = y + } + thread { + y = 1 + r2 = x + } + spec { + accept(0, 1) + accept(1, 0) + accept(1, 1) + forbid(0, 0) // redundant as forbidden is the default + } + } + +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublication.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublication.kt new file mode 100644 index 0000000..3659210 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublication.kt @@ -0,0 +1,134 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.autooutcomes.LitmusIOutcome +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.litmusTest +import kotlin.concurrent.Volatile + +@LitmusTestContainer +object UnsafePublication { + + private data class IntHolder(val x: Int = 0) + + val Plain = litmusTest({ + object : LitmusIOutcome() { + var h: IntHolder? = null + } + }) { + thread { + h = IntHolder() + } + thread { + r1 = h?.x ?: -1 + } + spec { + accept(0) + accept(-1) + } + } + + val VolatileAnnotated = litmusTest({ + object : LitmusIOutcome() { + @Volatile + var h: IntHolder? = null + } + }) { + thread { + h = IntHolder() + } + thread { + r1 = h?.x ?: -1 + } + spec { + accept(0) + accept(-1) + } + } + + val PlainWithConstructor = litmusTest({ + object : LitmusIOutcome() { + var h: IntHolder? = null + } + }) { + thread { + h = IntHolder(x = 1) + } + thread { + r1 = h?.x ?: -1 + } + spec { + accept(1) + accept(-1) + } + } + + val PlainArray = litmusTest({ + object : LitmusIOutcome() { + var arr: Array? = null + } + }) { + thread { + arr = Array(10) { 0 } + } + thread { + r1 = arr?.get(0) ?: -1 + } + spec { + accept(0) + accept(-1) + } + } + + private class RefHolder(val ref: IntHolder) + + val Reference = litmusTest({ + object : LitmusIOutcome() { + var h: RefHolder? = null + } + }) { + thread { + val ref = IntHolder(x = 1) + h = RefHolder(ref) + } + thread { + val t = h + r1 = t?.ref?.x ?: -1 + } + spec { + accept(1) + accept(-1) + } + } + + private class LeakingIntHolderContext { + var ih: LeakingIntHolder? = null + + inner class LeakingIntHolder { + val x: Int = 1 + + init { + ih = this + } + } + } + + val PlainWithLeakingConstructor = litmusTest({ + object : LitmusIOutcome() { + var ctx = LeakingIntHolderContext() + } + }) { + thread { + ctx.LeakingIntHolder() + } + thread { + r1 = ctx.ih?.x ?: -1 + } + spec { + accept(1) + accept(0) + accept(-1) + } + } + +} diff --git a/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/WordTearing.kt b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/WordTearing.kt new file mode 100644 index 0000000..741869f --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/WordTearing.kt @@ -0,0 +1,57 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIIIOutcome +import org.jetbrains.litmuskt.autooutcomes.LitmusZZOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.litmusTest + +@LitmusTestContainer +object WordTearing { + + val Array = litmusTest({ + object : LitmusZZOutcome() { + val arr = BooleanArray(2) + } + }) { + thread { + arr[0] = true + } + thread { + arr[1] = true + } + outcome { + r1 = arr[0] + r2 = arr[1] + this + } + spec { + accept(true, true) + } + } + + // source: https://github.com/openjdk/jcstress/blob/master/tests-custom/src/main/java/org/openjdk/jcstress/tests/tearing/ArrayInterleaveTest.java + val ArrayInterleave = litmusTest({ + object : LitmusIIIOutcome() { + val arr = ByteArray(256) + } + }) { + thread { + for (i in arr.indices step 2) arr[i] = 1 + } + thread { + for (i in 1.. r1++ + 1.toByte() -> r2++ + 2.toByte() -> r3++ + } + this + } + spec { + accept(0, 128, 128) + } + } +} diff --git a/testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/WordTearingNative.kt b/testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/WordTearingNative.kt new file mode 100644 index 0000000..cc4c926 --- /dev/null +++ b/testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/WordTearingNative.kt @@ -0,0 +1,35 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusZZOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.autooutcomes.interesting +import org.jetbrains.litmuskt.litmusTest + +@LitmusTestContainer +@OptIn(ObsoleteNativeApi::class) +object WordTearingNative { + + val Bitset = litmusTest({ + object : LitmusZZOutcome() { + val bs = BitSet() + } + }) { + thread { + bs.set(0) + } + thread { + bs.set(1) + } + outcome { + r1 = bs[0] + r2 = bs[1] + this + } + spec { + accept(true, true) + interesting(true, false) + interesting(false, true) + } + } +}