diff --git a/.github/workflows/run-litmus.yml b/.github/workflows/run-litmus.yml new file mode 100644 index 0000000..63483bd --- /dev/null +++ b/.github/workflows/run-litmus.yml @@ -0,0 +1,50 @@ +name: Run litmus tests on different platforms + +on: + push: + branches: + - dev-gh-ci + - development + - main + +# Note: this CI run is an "integration"-test or "smoke"-test. It is intended to verify that +# the basics of the tool work. It is NOT intended to be complete or to discover weak behaviors. + +jobs: + linux-run: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v4 + with: + distribution: oracle + java-version: 17 + - run: chmod +x gradlew + - name: Assemble CLI binary + run: ./gradlew cli:linkReleaseExecutableLinuxX64 + - name: Run litmus tests via CLI + run: ./cli/build/bin/linuxX64/releaseExecutable/cli.kexe -r pthread ".*" + - name: Run litmus tests with JCStress + # takes ~10 mins + run: ./gradlew :cli:jvmRun --args="-r jcstress -j '-m sanity' .*" + + macos-run: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v4 + with: + distribution: oracle + java-version: 17 + - run: chmod +x gradlew + - name: Assemble CLI binary (x64 + release) + run: ./gradlew cli:linkReleaseExecutableMacosX64 + - name: Run litmus tests via CLI (x64 + release) + run: ./cli/build/bin/macosX64/releaseExecutable/cli.kexe -r pthread ".*" + - name: Run litmus tests with JCStress + # takes ~10 mins + run: ./gradlew :cli:jvmRun --args="-r jcstress -j '-m sanity' .*" + - name: Assemble CLI binary (arm + release) + run: ./gradlew cli:linkReleaseExecutableMacosArm64 + - name: Run litmus tests via CLI (arm + release) + run: ./cli/build/bin/macosArm64/releaseExecutable/cli.kexe -r pthread ".*" diff --git a/.gitignore b/.gitignore index b741e1c..93f3879 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,5 @@ build/ temp/ *.hprof gitignored/ -litmus/src/nativeInterop/kaffinity.def -litmus/src/nativeInterop/kaffinity_gnu.o +local.properties +.kotlin diff --git a/ArmWorkerHalts.kt b/ArmWorkerHalts.kt deleted file mode 100644 index ab83a0a..0000000 --- a/ArmWorkerHalts.kt +++ /dev/null @@ -1,14 +0,0 @@ -import kotlin.native.concurrent.TransferMode -import kotlin.native.concurrent.Worker - -fun main() { - repeat(100_000) { i -> - val w = Worker.start() - val f = w.execute(TransferMode.SAFE, { i }) { - 5 + it - } - // w.requestTermination() - val r = f.result - println("future $i, result $r") - } -} \ No newline at end of file diff --git a/README.md b/README.md index d3bb0e8..d0dca4b 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 +./cli/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; +* 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,14 +85,18 @@ 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)) } + reset { + x = 0 + y = 0 + r1 = 0 + r2 = 0 + } } ``` @@ -99,25 +114,29 @@ 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. +* `spec { ... }` lambda classifies the outcomes into acceptable, interesting, and forbidden categories. +* `reset { ... }` lambda is used to reset the shared state to its original value, so that the test can be run again + using the same state instance. 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. - -* Since each test usually has its own specific state, it is quite useful to use anonymous classes for them. + 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. + Moreover, `r1` and `r2` will be automatically reset after running the test, so you don't need to manually write them + in the `reset` section. +* 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 convenient to use anonymous classes for them. Using these features, the test from above can be shortened as follows: @@ -142,58 +161,70 @@ val StoreBuffering: LitmusTest<*> = litmusTest({ accept(1, 1) interesting(0, 0) } + reset { + x = 0 + y = 0 + } } ``` ### 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..6df6864 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 "2.0.0" 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..8d1010c --- /dev/null +++ b/cli/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + kotlin("multiplatform") +} + +kotlin { + val nativeTargets = listOf( + linuxX64(), + // 1) no machine currently available 2) CLI library does not support +// linuxArm64(), + macosX64(), + macosArm64(), + mingwX64(), + ) + 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..bfed22e --- /dev/null +++ b/cli/src/commonMain/kotlin/org/jetbrains/litmuskt/CliCommon.kt @@ -0,0 +1,126 @@ +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) + val result = paramsList.map { params -> + runTest(params, test) + }.mergeResults() + echo(result.generateTable(), false) + echo("total count: ${result.totalCount()}, overall status: ${result.overallStatus()}") + echo() + } + } + + private fun runTest(params: LitmusRunParams, test: LitmusTest<*>): LitmusResult { + return when (parallelism) { + PARALLELISM_DISABLED -> { + // note: not running all tests here because of changing params + @Suppress("UNCHECKED_CAST") + runner.runTests(listOf(test) as List>, 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..e50b5c6 --- /dev/null +++ b/cli/src/jvmMain/kotlin/org/jetbrains/litmuskt/CliJvm.kt @@ -0,0 +1,95 @@ +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.runJCStressTests(tests, jcsParams) + + echo() + if (results.isEmpty()) { + echo("no tests were run, perhaps they are missing jcstress wrappers?", err = true) + return + } + results.forEach { (test, result) -> + echo("results for ${test.alias}:") + echo(result.generateTable() + "\n") + } + } + } +} 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 70% rename from litmus/src/nativeMain/kotlin/komem.litmus/CliNative.kt rename to cli/src/nativeMain/kotlin/org/jetbrains/litmuskt/CliNative.kt index 066c726..0aaab8b 100644 --- a/litmus/src/nativeMain/kotlin/komem.litmus/CliNative.kt +++ b/cli/src/nativeMain/kotlin/org/jetbrains/litmuskt/CliNative.kt @@ -1,16 +1,18 @@ -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.barriers.CinteropSpinBarrier 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 { + affinityManager?.let { schedulesMapped["short"] = it.presetShort() schedulesMapped["long"] = it.presetLong() } diff --git a/codegen/LICENSE b/codegen/LICENSE new file mode 100644 index 0000000..ffd0ab8 --- /dev/null +++ b/codegen/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 JetBrains Research + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/codegen/build.gradle.kts b/codegen/build.gradle.kts index ef3b11e..2a746f0 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:2.0.0-1.0.23") } 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/LICENSE b/core/LICENSE new file mode 100644 index 0000000..ffd0ab8 --- /dev/null +++ b/core/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 JetBrains Research + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..d30d26c --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + kotlin("multiplatform") + `java-library` +} + +kotlin { + val nativeTargets = listOf( + linuxX64(), + linuxArm64(), + macosX64(), + macosArm64(), + mingwX64(), + ) + + jvm { + withJava() + } + + 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")) + compilerOpts.addAll(listOf("-Wall", "-Werror")) + } + if (affinitySupported) { + create("affinity") { + defFile(project.file("src/nativeInterop/kaffinity.def")) + compilerOpts.add("-D_GNU_SOURCE") + compilerOpts.addAll(listOf("-Wall", "-Werror")) + } + } + create("kpthread") { + defFile(project.file("src/nativeInterop/kpthread.def")) + compilerOpts.addAll(listOf("-Wall", "-Werror")) + } + } + } + } + } + sourceSets { + commonTest { + dependencies { + implementation(kotlin("test")) + } + } + } +} diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/AffinityManager.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/AffinityManager.kt new file mode 100644 index 0000000..0e8facd --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/AffinityManager.kt @@ -0,0 +1,120 @@ +package org.jetbrains.litmuskt + +import kotlin.random.Random + +/** + * An interface for managing the CPU affinity of a [Threadlike] object. + */ +interface AffinityManager { + /** + * Binds a [Threadlike] to a certain set of CPU cores. + * + * @return `true` on success, `false` if setting affinity for [threadlike] is not supported + * @throws IllegalStateException if setting affinity is supported, but failed + */ + fun setAffinity(threadlike: Threadlike, cpus: Set): Boolean + + /** + * Gets the CPU cores this [threadlike] is bound to. + * + * @return the set of CPU cores on success, `null` if getting affinity for [threadlike] is not supported + * @throws IllegalStateException if getting affinity is supported, but failed + */ + fun getAffinity(threadlike: Threadlike): Set? +} + +/** + * Get the [AffinityManager] or null if it is not supported on this platform. + */ +expect val affinityManager: AffinityManager? + +/** + * Sets [threadlike]'s affinity to [cpus] and ensures that it worked. + * + * @return `true` on success, `false` if setting affinity for [threadlike] is not supported. + * @throws IllegalStateException if setting affinity is supported, but failed + */ +fun AffinityManager.setAffinityAndCheck(threadlike: Threadlike, cpus: Set): Boolean { + val set = setAffinity(threadlike, cpus) + if (set) { + getAffinity(threadlike)?.let { result -> + if (result == cpus) return true + error("affinity failed to set: expected $cpus, got $result") + } + } + return false +} + +/** + * When there are multiple threads, this interface manages which threads go to which CPU cores, so that + * we can achieve some "interesting" arrangements and thread interactions. Each thread should be assigned + * its own index, and with that index [allowedCores] provides the cores this thread should run on. + * + * We can then bind the thread to these cores using the [AffinityManager] interface. + */ +fun interface AffinityMap { + fun allowedCores(threadIndex: Int): Set +} + +/** + * Creates a map where each thread is assigned to one core, and each core + * is shifted by [shift] from the previous one. If cores overlap, the next free one is used. + * + * Example: with [shift]=2 and 6 cores in total the resulting map is: + * + * `[ {0}, {2}, {4}, {1}, {3}, {5} ]` + */ +fun AffinityManager.newShiftMap(shift: Int): AffinityMap = object : AffinityMap { + private val cpus: List> + + init { + val tmp = MutableList(cpuCount()) { setOf() } + var i = 0 + repeat(tmp.size) { + tmp[i] = setOf(i) + i = (i + shift) % tmp.size + if (tmp[i].isNotEmpty()) i++ + } + cpus = tmp + } + + override fun allowedCores(threadIndex: Int) = cpus[threadIndex] +} + +/** + * Create a map where each thread is assigned to one random core, without collisions. + */ +fun AffinityManager.newRandomMap(random: Random = Random): AffinityMap = object : AffinityMap { + private val cpus = (0.. = listOf( + newShiftMap(1), + newShiftMap(2), + newShiftMap(4), +) + +/** + * A longer list of some "reasonable" maps. Again, intended for using with test params variation. + */ +fun AffinityManager.presetLong(): List = List(cpuCount()) { newShiftMap(it) } + listOf( + object : AffinityMap { + override fun allowedCores(threadIndex: Int) = setOf(0, 1) + }, + object : AffinityMap { + override fun allowedCores(threadIndex: Int) = setOf(1, 2) + }, + object : AffinityMap { + override fun allowedCores(threadIndex: Int) = setOf( + (threadIndex * 2) % cpuCount(), + (threadIndex * 2 + 1) % cpuCount() + ) + } +) 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/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusOutcomeStats.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusOutcomeStats.kt new file mode 100644 index 0000000..8f562fd --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusOutcomeStats.kt @@ -0,0 +1,63 @@ +package org.jetbrains.litmuskt + +typealias LitmusOutcome = Any? + +enum class LitmusOutcomeType { ACCEPTED, INTERESTING, FORBIDDEN } + +data class LitmusOutcomeStats( + val outcome: LitmusOutcome, + val count: Long, + val type: LitmusOutcomeType, +) + +data class LitmusOutcomeSpec( + val accepted: Set, + val interesting: Set, + val forbidden: Set, + val default: LitmusOutcomeType, +) { + fun getType(outcome: LitmusOutcome) = when (outcome) { + in accepted -> LitmusOutcomeType.ACCEPTED + in interesting -> LitmusOutcomeType.INTERESTING + in forbidden -> LitmusOutcomeType.FORBIDDEN + else -> default + } + + val all = accepted + interesting + forbidden +} + +/** + * 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()`. + * + * See [LitmusAutoOutcome] file for `accept(r1, ...)` extension functions. The generic is used precisely for them. + */ +class LitmusOutcomeSpecScope { + private val accepted = mutableSetOf() + private val interesting = mutableSetOf() + private val forbidden = mutableSetOf() + private var default: LitmusOutcomeType? = null + + // note: if S is LitmusIOutcome, even single values should be interpreted as r1 + + fun accept(outcomes: Iterable) { + accepted.addAll(outcomes) + } + + fun interesting(outcomes: Iterable) { + interesting.addAll(outcomes) + } + + fun forbid(outcomes: Iterable) { + forbidden.addAll(outcomes) + } + + fun default(outcomeType: LitmusOutcomeType) { + if (default != null) + error("cannot set default outcome type more than once") + default = outcomeType + } + + fun build() = LitmusOutcomeSpec(accepted, interesting, forbidden, default ?: LitmusOutcomeType.FORBIDDEN) +} diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusResult.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusResult.kt new file mode 100644 index 0000000..924dbe2 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusResult.kt @@ -0,0 +1,44 @@ +package org.jetbrains.litmuskt + +typealias LitmusResult = List + +fun LitmusResult.generateTable(): String { + val totalCount = sumOf { it.count } + val table = this.sortedByDescending { it.count }.map { + val freq = it.count.toDouble() / totalCount + listOf( + it.outcome.toString(), + it.type.toString(), + it.count.toString(), + if (freq < 1e-5) "<0.001%" else "${(freq * 100).toString().take(6)}%" + ) + } + val tableHeader = listOf("outcome", "type", "count", "frequency") + return (listOf(tableHeader) + table).tableFormat(true) +} + +fun LitmusResult.totalCount() = sumOf { it.count } + +fun LitmusResult.overallStatus(): LitmusOutcomeType { + var isInteresting = false + for (stat in this) when (stat.type) { + LitmusOutcomeType.FORBIDDEN -> return LitmusOutcomeType.FORBIDDEN + LitmusOutcomeType.INTERESTING -> isInteresting = true + LitmusOutcomeType.ACCEPTED -> {} // ignore + } + return if (isInteresting) LitmusOutcomeType.INTERESTING else LitmusOutcomeType.ACCEPTED +} + +fun List.mergeResults(): LitmusResult { + data class LTOutcomeStatsAccumulator(var count: Long, val type: LitmusOutcomeType) + + val statMap = mutableMapOf() + for (stat in this.flatten()) { + val tempData = statMap.getOrPut(stat.outcome) { LTOutcomeStatsAccumulator(0L, stat.type) } + if (tempData.type != stat.type) { + error("merging conflicting stats: ${stat.outcome} is both ${stat.type} and ${tempData.type}") + } + tempData.count += stat.count + } + return statMap.map { (outcome, tempData) -> LitmusOutcomeStats(outcome, tempData.count, tempData.type) } +} diff --git a/litmus/src/commonMain/kotlin/komem/litmus/LitmusRunParams.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunParams.kt similarity index 92% rename from litmus/src/commonMain/kotlin/komem/litmus/LitmusRunParams.kt rename to core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunParams.kt index 411c6d8..dea9fc6 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, @@ -9,6 +7,7 @@ data class LitmusRunParams( val barrierProducer: BarrierProducer, ) +// TODO: rewrite (probably without sequence) fun variateRunParams( batchSizeSchedule: List, affinityMapSchedule: List, 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..f9e867a --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusRunner.kt @@ -0,0 +1,122 @@ +package org.jetbrains.litmuskt + +import kotlin.time.Duration + +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. + * + * It should also reset() the states after running the test. + */ + abstract fun startTest( + test: LitmusTest, + states: Array, + barrierProducer: BarrierProducer, + syncPeriod: Int, + affinityMap: AffinityMap?, + ): BlockingFuture +} + +/** + * 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. + * + * 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.runSingleTestParallel( + test: LitmusTest, + params: LitmusRunParams, + timeLimit: Duration = Duration.ZERO, + instances: Int = cpuCount() / test.threadCount, +): LitmusResult { + // allocate all states once, as it takes the most time compared to anything else + val allStates = List(instances) { + TypedArray(params.batchSize) { test.stateProducer() } + } + return repeatFor(timeLimit) { + 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, + ) + } + val allResults = allJoinHandles.map { it.await() } + return@repeatFor allResults.mergeResults() + }.mergeResults() +} + +/** + * Runs [tests] one by one, each with [params] and [timeLimit]. + * + * If [timeLimit] is not given, run each test once. + */ +inline fun LitmusRunner.runTests( + tests: List>, + params: LitmusRunParams, + timeLimit: Duration = Duration.ZERO, +): List = tests.map { test -> + val states = TypedArray(params.batchSize) { test.stateProducer() } + repeatFor(timeLimit) { + startTest( + test, states, params.barrierProducer, params.syncPeriod, params.affinityMap + ).await() + }.mergeResults() +} + +/** + * This function is only intended to be called from a runner, hence the receiver. It would have been better to + * make it `protected` inside [LitmusRunner], but we decided to keep that interface cleaner. + */ +fun LitmusRunner.calcStats( + states: Iterable, + 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)) + } +} diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTest.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTest.kt new file mode 100644 index 0000000..73e629e --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/LitmusTest.kt @@ -0,0 +1,62 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.autooutcomes.LitmusAutoOutcome + +data class LitmusTest( + val stateProducer: () -> S, + val threadFunctions: List Unit>, + val outcomeFinalizer: (S.() -> LitmusOutcome), + val outcomeSpec: LitmusOutcomeSpec, + val resetFunction: S.() -> Unit, +) { + val threadCount = threadFunctions.size +} + +class LitmusTestScope( + private val stateProducer: () -> S +) { + private val threadFunctions = mutableListOf Unit>() + private lateinit var outcomeFinalizer: S.() -> LitmusOutcome + private lateinit var outcomeSpec: LitmusOutcomeSpecScope + private lateinit var stateReset: S.() -> Unit + + fun thread(function: S.() -> Unit) { + threadFunctions.add(function) + } + + fun outcome(function: S.() -> LitmusOutcome) { + if (::outcomeFinalizer.isInitialized) error("cannot declare outcome more than once") + outcomeFinalizer = function + } + + fun spec(setup: LitmusOutcomeSpecScope.() -> Unit) { + if (::outcomeSpec.isInitialized) error("cannot declare spec more than once") + outcomeSpec = LitmusOutcomeSpecScope().apply(setup) + } + + fun reset(function: S.() -> Unit) { + if (::stateReset.isInitialized) error("cannot declare reset() more than once") + stateReset = function + } + + fun build(): LitmusTest { + if (threadFunctions.size < 2) error("tests require at least two threads") + if (!::outcomeSpec.isInitialized) error("spec not specified") + val outcomeFinalizer: S.() -> LitmusOutcome = when { + ::outcomeFinalizer.isInitialized -> outcomeFinalizer + stateProducer() is LitmusAutoOutcome -> { + { this } + } + else -> error("outcome not specified") + } + if (!::stateReset.isInitialized) error("reset() not specified") + val resetFunction: S.() -> Unit = { + stateReset() + (this as? LitmusAutoOutcome)?.outcomeReset() + } + return LitmusTest(stateProducer, threadFunctions, outcomeFinalizer, outcomeSpec.build(), resetFunction) + } +} + +fun litmusTest(stateProducer: () -> S, setup: LitmusTestScope.() -> Unit): LitmusTest<*> = + LitmusTestScope(stateProducer).apply(setup).build() diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Threadlike.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Threadlike.kt new file mode 100644 index 0000000..0309517 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Threadlike.kt @@ -0,0 +1,24 @@ +package org.jetbrains.litmuskt + +/** + * A "thread-like" is a wrapper for something that can be used as a thread, for example, Worker or pthread API. + * For now, returning a value from "threads" is not supported (as it is not currently needed). + */ +interface Threadlike { + /** + * Start running the function in a "thread". + * + * Notes: + * 1. This function should be only called once. + * 1. [function] should be non-capturing. + * 1. Since returning a value is not currently supported, the resulting future returns a stub (Unit). + * + * @return a "future" handle that will block when called until the "thread" has completed. + */ + fun start(args: A, function: (A) -> Unit): BlockingFuture + + /** + * Dispose of any resources the "thread" has allocated. Blocks until the resources are cleaned. + */ + fun dispose() +} diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/ThreadlikeRunner.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/ThreadlikeRunner.kt new file mode 100644 index 0000000..df40953 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/ThreadlikeRunner.kt @@ -0,0 +1,67 @@ +package org.jetbrains.litmuskt + +abstract class ThreadlikeRunner : LitmusRunner() { + + protected abstract fun threadlikeProducer(): Threadlike + + private fun threadFunction(threadContext: ThreadContext) = with(threadContext) { + val testFunction = test.threadFunctions[threadIndex] + for (i in states.indices) { + if (i % syncPeriod == 0) barrier.await() + states[i].testFunction() + } + // performance optimizations: + // 1) each thread takes a portion of states and calculates stats for it + // 2) each thread then resets these states + rangeResult = calcStats(states.view(resultCalcRange), test.outcomeSpec, test.outcomeFinalizer) + val resetFunction = test.resetFunction + for (i in resultCalcRange) states[i].resetFunction() + } + + private class ThreadContext( + val states: Array, + val test: LitmusTest, + val threadIndex: Int, + val syncPeriod: Int, + val barrier: Barrier, + val resultCalcRange: IntRange, + var rangeResult: LitmusResult? = null, + ) + + override fun startTest( + test: LitmusTest, + states: Array, + barrierProducer: BarrierProducer, + syncPeriod: Int, + affinityMap: AffinityMap? + ): BlockingFuture { + + val threads = List(test.threadCount) { threadlikeProducer() } + + val barrier = barrierProducer(test.threadCount) + val resultCalcRanges = states.indices.splitEqual(threads.size) + val contexts = List(threads.size) { i -> + val range = resultCalcRanges[i] + ThreadContext(states, test, i, syncPeriod, barrier, range) + } + + val futures = (threads zip contexts).map { (thread, context) -> + thread.start(context, ::threadFunction) + } + + // cannot set affinity before thread is started (because pthread_create has not been called yet) + affinityMap?.let { map -> + affinityManager?.apply { + for ((i, t) in threads.withIndex()) { + setAffinityAndCheck(t, map.allowedCores(i)) + } + } + } + + return BlockingFuture { + futures.forEach { it.await() } // await all results + threads.forEach { it.dispose() } // stop all "threads" + contexts.map { it.rangeResult!! }.mergeResults() + } + } +} diff --git a/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Utils.kt b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Utils.kt new file mode 100644 index 0000000..5bec8c1 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/Utils.kt @@ -0,0 +1,80 @@ +package org.jetbrains.litmuskt + +import kotlin.time.Duration +import kotlin.time.TimeSource + +fun List>.tableFormat(hasHeader: Boolean = false): String { + val columnCount = maxOf { it.size } + val columnSizes = (0.. + this.mapNotNull { it.getOrNull(i) }.maxOf { it.length } + 2 + } + return buildString { + this@tableFormat.forEach { row -> + row.forEachIndexed { i, word -> + val startPadding = (columnSizes[i] - word.length) / 2 + val endPadding = columnSizes[i] - word.length - startPadding + append(" ".repeat(startPadding)) + append(word) + append(" ".repeat(endPadding)) + if (i != row.size - 1) append("|") + } + appendLine() + if (hasHeader && row === this@tableFormat.first()) { + appendLine("-".repeat(columnSizes.sum() + columnCount - 1)) + } + } + } +} + +expect fun cpuCount(): Int + +@Suppress("UNCHECKED_CAST") +fun TypedArray(size: Int, init: (Int) -> S): Array = Array(size, init) as Array + +/** + * Returns a lazy iterable that iterates over a portion of the underlying array. + */ +fun Array.view(range: IntRange): Iterable { + return Iterable { + object : Iterator { + private val delegate = range.iterator() + override fun hasNext() = delegate.hasNext() + override fun next(): S = this@view[delegate.nextInt()] + } + } +} + +/** + * Split a range into [n] parts of equal (+/- 1) length. + */ +fun IntRange.splitEqual(n: Int): List { + val size = endInclusive - start + 1 + val len = size / n // base length of each sub-range + val remainder = size % n + val delim = start + (len + 1) * remainder // delimiter between lengths (l+1) and l + return List(n) { i -> + if (i < remainder) { + (start + i * (len + 1))..<(start + (i + 1) * (len + 1)) + } else { + val j = i - remainder + (delim + j * len)..<(delim + (j + 1) * len) + } + } +} + +/** + * A future that blocks on calling [await]. + */ +fun interface BlockingFuture { + fun await(): T +} + +/** + * Repeat a function for at least the given [duration]. Runs the function at least once. + */ +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/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..d6df406 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/BooleanAutoOutcomes.kt @@ -0,0 +1,41 @@ +package org.jetbrains.litmuskt.autooutcomes + +import org.jetbrains.litmuskt.LitmusOutcomeSpecScope + +/** + * "Z" is the name for Boolean outcomes in JCStress. + */ + +// TODO: codegen + +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 outcomeReset() { + r1 = false + r2 = false + } + + final override fun toList() = listOf(r1, r2) + final override fun parseOutcome(str: String): LitmusZZOutcome { + val rs = str.split(", ").map(String::toBooleanStrict) + return LitmusZZOutcome(rs[0], rs[1]) + } +} + +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..556beea --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/IntAutoOutcomes.kt @@ -0,0 +1,135 @@ +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 outcomeReset() { + r1 = 0 + } + + final override fun toList() = listOf(r1) + final override fun parseOutcome(str: String): LitmusIOutcome { + val rs = str.split(", ").map(String::toInt) + return LitmusIOutcome(rs[0]) + } +} + +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 outcomeReset() { + r1 = 0 + r2 = 0 + } + + final override fun toList() = listOf(r1, r2) + final override fun parseOutcome(str: String): LitmusIIOutcome { + val rs = str.split(", ").map(String::toInt) + return LitmusIIOutcome(rs[0], rs[1]) + } +} + +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 outcomeReset() { + r1 = 0 + r2 = 0 + r3 = 0 + } + + final override fun toList() = listOf(r1, r2, r3) + final override fun parseOutcome(str: String): LitmusIIIOutcome { + val rs = str.split(", ").map(String::toInt) + return LitmusIIIOutcome(rs[0], rs[1], rs[2]) + } +} + +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 outcomeReset() { + r1 = 0 + r2 = 0 + r3 = 0 + r4 = 0 + } + + final override fun toList() = listOf(r1, r2, r3, r4) + final override fun parseOutcome(str: String): LitmusIIIIOutcome { + val rs = str.split(", ").map(String::toInt) + return LitmusIIIIOutcome(rs[0], rs[1], rs[2], rs[3]) + } +} + +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..53bef17 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LitmusAutoOutcome.kt @@ -0,0 +1,32 @@ +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 + + fun outcomeReset() + + // for JCStress interop + fun toList(): List + fun parseOutcome(str: String): LitmusAutoOutcome +} + 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..bc79b35 --- /dev/null +++ b/core/src/commonMain/kotlin/org/jetbrains/litmuskt/autooutcomes/LongAutoOutcomes.kt @@ -0,0 +1,135 @@ +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 outcomeReset() { + r1 = 0 + } + + final override fun toList() = listOf(r1) + final override fun parseOutcome(str: String): LitmusLOutcome { + val rs = str.split(", ").map(String::toLong) + return LitmusLOutcome(rs[0]) + } +} + +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 outcomeReset() { + r1 = 0 + r2 = 0 + } + + final override fun toList() = listOf(r1, r2) + final override fun parseOutcome(str: String): LitmusLLOutcome { + val rs = str.split(", ").map(String::toLong) + return LitmusLLOutcome(rs[0], rs[1]) + } +} + +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 outcomeReset() { + r1 = 0 + r2 = 0 + r3 = 0 + } + + final override fun toList() = listOf(r1, r2, r3) + final override fun parseOutcome(str: String): LitmusLLLOutcome { + val rs = str.split(", ").map(String::toLong) + return LitmusLLLOutcome(rs[0], rs[1], rs[2]) + } +} + +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 outcomeReset() { + r1 = 0 + r2 = 0 + r3 = 0 + r4 = 0 + } + + final override fun toList() = listOf(r1, r2, r3, r4) + final override fun parseOutcome(str: String): LitmusLLLLOutcome { + val rs = str.split(", ").map(String::toLong) + return LitmusLLLLOutcome(rs[0], rs[1], rs[2], rs[3]) + } +} + +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/commonTest/kotlin/org/jetbrains/litmuskt/UtilTest.kt b/core/src/commonTest/kotlin/org/jetbrains/litmuskt/UtilTest.kt new file mode 100644 index 0000000..e372afc --- /dev/null +++ b/core/src/commonTest/kotlin/org/jetbrains/litmuskt/UtilTest.kt @@ -0,0 +1,28 @@ +package org.jetbrains.litmuskt + +import kotlin.test.Test +import kotlin.test.assertEquals + +class UtilTest { + + @Test + fun testRangeSplitEqual() { + assertEquals( + listOf(0..<4, 4..<8, 8..<11), + (0..<11).splitEqual(3) + ) + assertEquals( + listOf(0..<3, 3..<5, 5..<7, 7..<9, 9..<11), + (0..<11).splitEqual(5) + ) + assertEquals( + listOf(0..<500, 500..<1000), + (0..<1000).splitEqual(2) + ) + assertEquals( + listOf(1..1, 2..2), + (1..2).splitEqual(2) + ) + } + +} diff --git a/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/AffinityManagerImplNoop.kt b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/AffinityManagerImplNoop.kt new file mode 100644 index 0000000..4e33292 --- /dev/null +++ b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/AffinityManagerImplNoop.kt @@ -0,0 +1,3 @@ +package org.jetbrains.litmuskt + +actual val affinityManager: AffinityManager? = null 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..a7aba68 --- /dev/null +++ b/core/src/jvmMain/kotlin/org/jetbrains/litmuskt/JvmThreadRunner.kt @@ -0,0 +1,33 @@ +package 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? + ): BlockingFuture { + 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 BlockingFuture { + threads.forEach { it.join() } + calcStats(states.asIterable(), 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..ca79cf5 --- /dev/null +++ b/core/src/linuxMain/kotlin/org/jetbrains/litmuskt/AffinityBindingsImplPosix.kt @@ -0,0 +1,50 @@ +package org.jetbrains.litmuskt + +import kaffinity.* +import kotlinx.cinterop.* +import platform.posix.cpu_set_t +import platform.posix.pthread_t +import kotlin.native.concurrent.ObsoleteWorkersApi + +@OptIn(ExperimentalStdlibApi::class, ObsoleteWorkersApi::class, ExperimentalForeignApi::class) +actual val affinityManager: AffinityManager? = object : AffinityManager { + + override fun setAffinity(threadlike: Threadlike, cpus: Set): Boolean { + when (threadlike) { + is WorkerThreadlike -> memScoped { + val pthreadPtr = alloc(threadlike.worker.platformThreadId).ptr + setPthreadAffinity(pthreadPtr, cpus) + } + is PthreadThreadlike -> setPthreadAffinity(threadlike.pthreadPtr, cpus) + else -> return false + } + return true + } + + override fun getAffinity(threadlike: Threadlike): Set? = when (threadlike) { + is WorkerThreadlike -> memScoped { + val pthreadPtr = alloc(threadlike.worker.platformThreadId).ptr + getPthreadAffinity(pthreadPtr) + } + is PthreadThreadlike -> getPthreadAffinity(threadlike.pthreadPtr) + else -> null + } + + @OptIn(ExperimentalForeignApi::class) + private fun setPthreadAffinity(threadPtr: CPointer<*>, 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(threadPtr, set.ptr).syscallCheck() + } + + @OptIn(ExperimentalForeignApi::class) + private fun getPthreadAffinity(threadPtr: CPointer<*>): Set = memScoped { + val set = alloc() + get_affinity(threadPtr, set.ptr).syscallCheck() + return (0..() + GetSystemInfo(systemInfo.ptr) + systemInfo.dwNumberOfProcessors.toInt() +} diff --git a/litmus/src/nativeInterop/barrier.def b/core/src/nativeInterop/barrier.def similarity index 100% rename from litmus/src/nativeInterop/barrier.def rename to core/src/nativeInterop/barrier.def diff --git a/litmus/src/nativeInterop/barrier.h b/core/src/nativeInterop/barrier.h similarity index 100% rename from litmus/src/nativeInterop/barrier.h rename to core/src/nativeInterop/barrier.h diff --git a/core/src/nativeInterop/kaffinity.def b/core/src/nativeInterop/kaffinity.def new file mode 100644 index 0000000..acad8c0 --- /dev/null +++ b/core/src/nativeInterop/kaffinity.def @@ -0,0 +1,25 @@ +--- + +#include + +int set_affinity(void* thread, cpu_set_t* set) { + pthread_t pthread = *((pthread_t*) thread); + return pthread_setaffinity_np(pthread, sizeof(*set), set); +} +int get_affinity(void* thread, cpu_set_t* set) { + pthread_t pthread = *((pthread_t*) thread); + return pthread_getaffinity_np(pthread, sizeof(*set), set); +} + +void cpu_zero(cpu_set_t* set) { + CPU_ZERO(set); +} +void cpu_set(int cpu, cpu_set_t* set) { + CPU_SET(cpu, set); +} +int cpu_isset(int cpu, cpu_set_t* set) { + return CPU_ISSET(cpu, set); +} +int 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/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..36157a0 --- /dev/null +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/NativeUtils.kt @@ -0,0 +1,14 @@ +package org.jetbrains.litmuskt + +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.toKString +import platform.posix.errno +import platform.posix.strerror + +@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..dd31908 --- /dev/null +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadRunner.kt @@ -0,0 +1,5 @@ +package org.jetbrains.litmuskt + +class PthreadRunner : ThreadlikeRunner() { + override fun threadlikeProducer(): Threadlike = PthreadThreadlike() +} diff --git a/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadThreadlike.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadThreadlike.kt new file mode 100644 index 0000000..7631284 --- /dev/null +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/PthreadThreadlike.kt @@ -0,0 +1,39 @@ +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 + +@OptIn(ExperimentalForeignApi::class) +class PthreadThreadlike : Threadlike { + + val pthreadPtr = k_pthread_t_alloc() ?: error("could not allocate pthread_t pointer") + + private class ThreadData(val args: A, val function: (A) -> Unit) + + override fun start(args: A, function: (A) -> Unit): BlockingFuture { + val threadData = ThreadData(args, function) + val threadDataRef = StableRef.create(threadData) + + k_pthread_create( + pthreadPtr, + staticCFunction { + val data = it!!.asStableRef>().get() + data.function(data.args) + return@staticCFunction null + }, + threadDataRef.asCPointer() + ).syscallCheck() + + return BlockingFuture { + k_pthread_join(pthreadPtr, null).syscallCheck() + threadDataRef.dispose() + } + } + + override fun dispose() { + k_pthread_t_free(pthreadPtr) + } +} diff --git a/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerRunner.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerRunner.kt new file mode 100644 index 0000000..40edab7 --- /dev/null +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerRunner.kt @@ -0,0 +1,5 @@ +package org.jetbrains.litmuskt + +class WorkerRunner : ThreadlikeRunner() { + override fun threadlikeProducer(): Threadlike = WorkerThreadlike() +} diff --git a/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerThreadlike.kt b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerThreadlike.kt new file mode 100644 index 0000000..7c4f1f3 --- /dev/null +++ b/core/src/nativeMain/kotlin/org/jetbrains/litmuskt/WorkerThreadlike.kt @@ -0,0 +1,29 @@ +package org.jetbrains.litmuskt + +import kotlin.native.concurrent.ObsoleteWorkersApi +import kotlin.native.concurrent.TransferMode +import kotlin.native.concurrent.Worker + +@OptIn(ObsoleteWorkersApi::class) +class WorkerThreadlike : Threadlike { + + val worker = Worker.start() + + private data class WorkerContext( + val args: A, + val threadFunction: (A) -> Unit, + ) + + override fun start(args: A, function: (A) -> Unit): BlockingFuture { + val context = WorkerContext(args, function) + val future = worker.execute( + TransferMode.SAFE /* ignored */, + { context } + ) { (a, f) -> f(a) } + return BlockingFuture { future.result } + } + + override fun dispose() { + worker.requestTermination().result + } +} 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/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/BarrierTest.kt b/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/BarrierTest.kt new file mode 100644 index 0000000..ee9f11d --- /dev/null +++ b/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/BarrierTest.kt @@ -0,0 +1,59 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.barriers.CinteropSpinBarrier +import org.jetbrains.litmuskt.barriers.KNativeSpinBarrier +import platform.posix.sleep +import kotlin.concurrent.Volatile +import kotlin.test.Test +import kotlin.test.assertEquals + +class BarrierTest { + + fun testBarrier(barrierProducer: (Int) -> Barrier) { + val t1 = PthreadThreadlike() + val t2 = PthreadThreadlike() + + class Context( + val barrier: Barrier, + var x: Int = 0, + var y: Int = 0, + @Volatile + var flush: Int = 0 // ensure that changes are visible + ) + + val ctx = Context(barrierProducer(2)) + + val f1 = t1.start(ctx) { + it.barrier.await() // sync #1 + it.x = 1 + it.flush++ + + sleep(2u) + assertEquals(0, it.y) // thread 2 is waiting + it.barrier.await() // sync #2 + sleep(1u) + assertEquals(1, it.y) // thread 2 has continued + } + val f2 = t2.start(ctx) { + sleep(1u) + assertEquals(0, it.x) // thread 1 is waiting + it.barrier.await() // sync #1 + sleep(1u) + assertEquals(1, it.x) // thread 1 has continued + + it.barrier.await() // sync #2 + it.y = 1 + it.flush++ + } + + f1.await() + f2.await() + } + + @Test + fun testCinteropSpinBarrier() = testBarrier { CinteropSpinBarrier(it) } + + @Test + fun testKNativeSpinBarrier() = testBarrier { KNativeSpinBarrier(it) } + +} diff --git a/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/IntegrationTest.kt b/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/IntegrationTest.kt new file mode 100644 index 0000000..8892d5b --- /dev/null +++ b/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/IntegrationTest.kt @@ -0,0 +1,51 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.autooutcomes.LitmusIIOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.barriers.CinteropSpinBarrier +import kotlin.test.Test +import kotlin.test.assertNotEquals + +class IntegrationTest { + + private val sampleLitmusTest = litmusTest({ + object : LitmusIIOutcome() { + var x = 0 + } + }) { + thread { + x = 1 + } + thread { + r1 = x + x = 2 + r2 = x + } + spec { + accept(0, 2) + accept(1, 2) + accept(0, 1) // r1 = 0; x = 2; x = 1 (t1); r2 = 1 + } + reset { + x = 0 + } + } + + @Test + fun testBasic() { + val runner = PthreadRunner() + + @Suppress("UNCHECKED_CAST") + val result = runner.runTests( + tests = listOf(sampleLitmusTest) as List>, + params = LitmusRunParams( + batchSize = 1_000_000, + syncPeriod = 10, + affinityMap = null, + barrierProducer = ::CinteropSpinBarrier + ) + ).first() + assertNotEquals(LitmusOutcomeType.FORBIDDEN, result.overallStatus()) + } + +} diff --git a/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/ThreadlikeTest.kt b/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/ThreadlikeTest.kt new file mode 100644 index 0000000..fe296db --- /dev/null +++ b/core/src/nativeTest/kotlin/org/jetbrains/litmuskt/ThreadlikeTest.kt @@ -0,0 +1,27 @@ +package org.jetbrains.litmuskt + +import platform.posix.sleep +import kotlin.test.Test +import kotlin.test.assertEquals + +class ThreadlikeTest { + + private fun testThreadlike(t: Threadlike) { + class IntHolder(var x: Int) + + val holder = IntHolder(0) + val future = t.start(holder) { + sleep(1u) + it.x = 1 + } + assertEquals(0, holder.x) // checking parallelism + future.await() + assertEquals(1, holder.x) + } + + @Test + fun testWorkerThreadlike() = testThreadlike(WorkerThreadlike()) + + @Test + fun testPthreadThreadlike() = testThreadlike(PthreadThreadlike()) +} 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/herdtests/CoRR.litmus b/herdtests/CoRR.litmus deleted file mode 100644 index c01a77f..0000000 --- a/herdtests/CoRR.litmus +++ /dev/null @@ -1,16 +0,0 @@ -AArch64 CoRR -"Rfe PosRR Fre" -Cycle=Rfe PosRR Fre -Relax= -Safe=Rfe Fre PosRR -Prefetch= -Com=Rf Fr -Orig=Rfe PosRR Fre -{ -0:X1=x; -1:X1=x; -} - P0 | P1 ; - MOV W0,#1 | LDR W0,[X1] ; - STR W0,[X1] | LDR W2,[X1] ; -exists (not (x=1 /\ (1:X0=0 /\ (1:X2=0 \/ 1:X2=1) \/ 1:X0=1 /\ 1:X2=1))) diff --git a/herdtests/IRIW.litmus b/herdtests/IRIW.litmus deleted file mode 100644 index 2e9899a..0000000 --- a/herdtests/IRIW.litmus +++ /dev/null @@ -1,13 +0,0 @@ -AArch64 IRIW+o-o+o-o -Mapping=0:X2=r2,0:X0=r1,1:X2=r4,1:X0=r3, -Hash=eec5da8967890ec82af176d7186fd1c5 - -{0:X3=y; 0:X1=x; 1:X3=x; 1:X1=y; 2:X0=x; 3:X0=y; x=0; y=0;} - - P0 | P1 | P2 | P3 ; - LDR W0,[X1] | LDR W0,[X1] | MOV W1,#1 | MOV W1,#1 ; - LDR W2,[X3] | LDR W2,[X3] | STR W1,[X0] | STR W1,[X0] ; - - - -exists (0:X0=1 /\ 0:X2=0 /\ 1:X0=1 /\ 1:X2=0) diff --git a/herdtests/LB.litmus b/herdtests/LB.litmus deleted file mode 100644 index ae7dc5a..0000000 --- a/herdtests/LB.litmus +++ /dev/null @@ -1,18 +0,0 @@ -AArch64 LB -"PodRW Rfe PodRW Rfe" -Cycle=Rfe PodRW Rfe PodRW -Relax= -Safe=Rfe PodRW -Prefetch=0:x=F,0:y=W,1:y=F,1:x=W -Com=Rf Rf -Orig=PodRW Rfe PodRW Rfe -{ -0:X1=x; 0:X3=y; -1:X1=y; 1:X3=x; -} - P0 | P1 ; - LDR W0,[X1] | LDR W0,[X1] ; - MOV W2,#1 | MOV W2,#1 ; - STR W2,[X3] | STR W2,[X3] ; -exists -(0:X0=1 /\ 1:X0=1) diff --git a/herdtests/MP.litmus b/herdtests/MP.litmus deleted file mode 100644 index e274ea5..0000000 --- a/herdtests/MP.litmus +++ /dev/null @@ -1,62 +0,0 @@ -AArch64 MP -"PodWW Rfe PodRR Fre" - -(* This is a two-thread message-passing (MP) test: Thread 0 writes to -x and y, while Thread 1 reads from y and x. The interesting question -is whether Thread 1 can see Thread 0's write to y and (in the same -execution) read x from the initial state.*) - -(* The initial-state setup, of memory values (here x and y are -implicitly initially 0) and of register values for each thread: *) -{ -0:X1=x; 0:X3=y; -1:X1=y; 1:X3=x; -} - -(* The assembly code for each thread: *) - P0 | P1 ; - MOV W0,#1 | LDR W0,[X1] (* Ry=1 *) ; - STR W0,[X1] (* Wx=1 *) | LDR W2,[X3] (* Rx=0 *) ; - MOV W2,#1 | ; - STR W2,[X3] (* Wy=1 *) | ; - -(* The final-state condition, identifying the interesting execution: *) - -exists -(1:X0=1 /\ 1:X2=0) - - -(* In general the final condition might be allowed or forbidden on any -specific model, and might be observable or not observable -experimentally on any specific hardware implementation. For this -test, the final condition identifies the execution shown in the -comments, in which Thread 1 (P1) sees the Thread 0 write of y=1 but -reads the initial-state value x=0 for x, without seeing the Thread 0 -write of x=1. This is a non-sequentially-consistent execution; it is -allowed by the ARM architecture and observable on many ARM -implementations. *) - -(* This test was generated by the "diy" tool from the description, on -the second line, of a cycle of edges in a potential -non-sequentially-consistent execution: - - PodWW : a program-order edge (Po), between two write accesses (WW) - to different addresses (d) - Rfe : a reads-from edge (Rf) that is "external" (e), ie inter-thread - PodRR : a program-order edge (Po), between two read accesses (RR) - from different addresses (d) - Fre : a from-reads edge, from a read to a coherence successor of the - write it reads from, that is "external" (e), ie inter-thread -*) - -(* The following is additional data from the generation process, and a -prefetch hint that can be useful when running the test experimentally -on hardware. - -Cycle=Rfe PodRR Fre PodWW -Relax= -Safe=Rfe Fre PodWW PodRR -Prefetch=0:x=F,0:y=W,1:y=F,1:x=T -Com=Rf Fr -Orig=PodWW Rfe PodRR Fre -*) \ No newline at end of file diff --git a/herdtests/SB.litmus b/herdtests/SB.litmus deleted file mode 100644 index 10b9b87..0000000 --- a/herdtests/SB.litmus +++ /dev/null @@ -1,18 +0,0 @@ -AArch64 SB -"PodWR Fre PodWR Fre" -Cycle=Fre PodWR Fre PodWR -Relax= -Safe=Fre PodWR -Prefetch=0:x=F,0:y=T,1:y=F,1:x=T -Com=Fr Fr -Orig=PodWR Fre PodWR Fre -{ -0:X1=x; 0:X3=y; -1:X1=y; 1:X3=x; -} - P0 | P1 ; - MOV W0,#1 | MOV W0,#1 ; - STR W0,[X1] | STR W0,[X1] ; - LDR W2,[X3] | LDR W2,[X3] ; -exists -(0:X2=0 /\ 1:X2=0) 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..6f035ee --- /dev/null +++ b/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/Codegen.kt @@ -0,0 +1,152 @@ +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:\n" + e.stackTraceToString()) + 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 outcomeVarTypes = outcomeTypeName.map { c -> + when (c) { + 'I' -> "Integer" + 'L' -> "Long" + 'Z' -> "Boolean" + // TODO: add others once they are created + else -> error("unrecognized outcome type '$c'") + } + } + + 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 result = (List) (Object) ((LitmusAutoOutcome) fA.invoke(state)).toList(); + ${List(outcomeVarTypes.size) { "r.r${it + 1} = (${outcomeVarTypes[it]}) 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..d9dd3fe --- /dev/null +++ b/jcstress-wrapper/src/main/kotlin/org/jetbrains/litmuskt/JCStressRunner.kt @@ -0,0 +1,155 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.autooutcomes.LitmusAutoOutcome +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? + ): BlockingFuture { + throw NotImplementedError("jcstress runner should not be called with explicit params like this") + } + + internal fun startTests( + tests: List>, + params: LitmusRunParams + ): BlockingFuture, LitmusResult>> { + 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 BlockingFuture { + jcs.waitFor() + if (jcs.exitValue() != 0) error("jcstress exited with code ${jcs.exitValue()}") + // not all tests might have generated wrappers + return@BlockingFuture tests + .associateWith { test -> parseJCStressResults(test) } + .filterValues { it != null } + .mapValues { (_, result) -> result!! } // remove nullable type + } + } + + /** + * 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" + if (Files.notExists(resultsFile)) return null + var lines = Files.lines(resultsFile).asSequence() + + // 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 outcomeParser = test.stateProducer() as LitmusAutoOutcome + val outcomesOrdered = linesOutcomes.map { + val outcomeString = parseElementData(it) + outcomeParser.parseOutcome(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('>', '<') +} + +/** + * Use this function instead of [runTests] when running multiple tests with JCStress. This function avoids restarting + * JCStress for each individual test and instead submits them all at the same time. + */ +fun JCStressRunner.runJCStressTests( + tests: List>, + params: LitmusRunParams, +): Map, LitmusResult> = startTests(tests, params).await() + +/** + * 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..820187d --- /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 + 2.0.0 + + + 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/AffinityMap.kt b/litmus/src/commonMain/kotlin/komem/litmus/AffinityMap.kt deleted file mode 100644 index bcdf0e1..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/AffinityMap.kt +++ /dev/null @@ -1,5 +0,0 @@ -package komem.litmus - -fun interface AffinityMap { - fun allowedCores(threadIndex: Int): Set -} 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/LitmusOutcomeStats.kt b/litmus/src/commonMain/kotlin/komem/litmus/LitmusOutcomeStats.kt deleted file mode 100644 index 96e7a83..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/LitmusOutcomeStats.kt +++ /dev/null @@ -1,101 +0,0 @@ -package komem.litmus - -typealias LitmusOutcome = Any? - -enum class LitmusOutcomeType { ACCEPTED, INTERESTING, FORBIDDEN } - -data class LitmusOutcomeStats( - val outcome: LitmusOutcome, - val count: Long, - val type: LitmusOutcomeType?, -) - -data class LitmusOutcomeSpec( - val accepted: Set, - val interesting: Set, - val forbidden: Set, - val default: LitmusOutcomeType, -) { - fun getType(outcome: LitmusOutcome) = when (outcome) { - in accepted -> LitmusOutcomeType.ACCEPTED - in interesting -> LitmusOutcomeType.INTERESTING - in forbidden -> LitmusOutcomeType.FORBIDDEN - else -> default - } -} - -/** - * For convenience, it is possible to use `accept(vararg values)` if test outcome is a `List`. - * This is true for [LitmusAutoOutcome]. - * - * Use `accept(value)` otherwise. Notice that `accept(a, b)` is NOT the same as `accept(a); accept(b)`. - * - * The same applies to `interesting()` and `forbid()`. - */ -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) - } - - fun interesting(vararg outcome: LitmusOutcome) { - interesting.add(outcome.toList()) - } - - fun forbid(outcome: LitmusOutcome) { - forbidden.add(outcome) - } - - fun forbid(vararg outcome: LitmusOutcome) { - forbidden.add(outcome.toList()) - } - - fun default(outcomeType: LitmusOutcomeType) { - if (default != null) - error("cannot set default outcome type more than once") - default = outcomeType - } - - fun build() = LitmusOutcomeSpec(accepted, interesting, forbidden, default ?: LitmusOutcomeType.FORBIDDEN) -} - -typealias LitmusResult = List - -fun LitmusResult.generateTable(): String { - val totalCount = sumOf { it.count } - val table = this.sortedByDescending { it.count }.map { - val freq = it.count.toDouble() / totalCount - listOf( - it.outcome.toString(), - it.type.toString(), - it.count.toString(), - if (freq < 1e-5) "<0.001%" else "${(freq * 100).toString().take(6)}%" - ) - } - val tableHeader = listOf("outcome", "type", "count", "frequency") - return (listOf(tableHeader) + table).tableFormat(true) -} - -fun List.mergeResults(): LitmusResult { - data class LTOutcomeStatTempData(var count: Long, var type: LitmusOutcomeType?) - - val statMap = mutableMapOf() - for (stat in this.flatten()) { - val tempData = statMap.getOrPut(stat.outcome) { LTOutcomeStatTempData(0L, stat.type) } - if (tempData.type != stat.type) error("merging conflicting stats: ${stat.outcome} is both ${stat.type} and ${tempData.type}") - tempData.count += stat.count - } - return statMap.map { (outcome, tempData) -> LitmusOutcomeStats(outcome, tempData.count, tempData.type) } -} 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/LitmusTest.kt b/litmus/src/commonMain/kotlin/komem/litmus/LitmusTest.kt deleted file mode 100644 index bd04a6a..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/LitmusTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package komem.litmus - -data class LitmusTest( - val stateProducer: () -> S, - val threadFunctions: List Any?>, - val outcomeFinalizer: (S.() -> LitmusOutcome), - val outcomeSpec: LitmusOutcomeSpec -) { - val threadCount = threadFunctions.size -} - -class LitmusTestScope( - private val stateProducer: () -> S -) { - private val threadFunctions = mutableListOf Any?>() - private lateinit var outcomeFinalizer: S.() -> LitmusOutcome - private lateinit var outcomeSpec: LitmusOutcomeSpecScope - - fun thread(function: S.() -> Unit) { - threadFunctions.add(function) - } - - fun outcome(function: S.() -> LitmusOutcome) { - if (::outcomeFinalizer.isInitialized) error("cannot set outcome more than once") - outcomeFinalizer = function - } - - fun spec(setup: LitmusOutcomeSpecScope.() -> Unit) { - if (::outcomeSpec.isInitialized) error("cannot set spec more than once") - outcomeSpec = LitmusOutcomeSpecScope().apply(setup) - } - - fun build(): LitmusTest { - if (threadFunctions.size < 2) error("tests require at least two threads") - if (!::outcomeSpec.isInitialized) error("spec not specified") - val outcomeFinalizer: S.() -> LitmusOutcome = when { - ::outcomeFinalizer.isInitialized -> outcomeFinalizer - stateProducer() is LitmusAutoOutcome -> { - { (this as LitmusAutoOutcome).getOutcome() } - } - - else -> error("outcome not specified") - } - return LitmusTest(stateProducer, threadFunctions, outcomeFinalizer, outcomeSpec.build()) - } -} - -fun litmusTest(stateProducer: () -> S, setup: LitmusTestScope.() -> Unit) = - LitmusTestScope(stateProducer).apply(setup).build() diff --git a/litmus/src/commonMain/kotlin/komem/litmus/Utils.kt b/litmus/src/commonMain/kotlin/komem/litmus/Utils.kt deleted file mode 100644 index 647df87..0000000 --- a/litmus/src/commonMain/kotlin/komem/litmus/Utils.kt +++ /dev/null @@ -1,26 +0,0 @@ -package komem.litmus - -fun List>.tableFormat(hasHeader: Boolean = false): String { - val columnCount = maxOf { it.size } - val columnSizes = (0.. - this.mapNotNull { it.getOrNull(i) }.maxOf { it.length } + 2 - } - return buildString { - this@tableFormat.forEach { row -> - row.forEachIndexed { i, word -> - val startPadding = (columnSizes[i] - word.length) / 2 - val endPadding = columnSizes[i] - word.length - startPadding - append(" ".repeat(startPadding)) - append(word) - append(" ".repeat(endPadding)) - if (i != row.size - 1) append("|") - } - appendLine() - if (hasHeader && row === this@tableFormat.first()) { - appendLine("-".repeat(columnSizes.sum() + columnCount - 1)) - } - } - } -} - -expect fun cpuCount(): Int 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.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.c b/litmus/src/nativeInterop/kaffinity_gnu.c deleted file mode 100644 index 3fd1f16..0000000 --- a/litmus/src/nativeInterop/kaffinity_gnu.c +++ /dev/null @@ -1,23 +0,0 @@ -#define _GNU_SOURCE - -#include "kaffinity.h" - -int set_affinity(pthread_t thread, cpu_set_t* set) { - return pthread_setaffinity_np(thread, sizeof(*set), set); -} -int get_affinity(pthread_t thread, cpu_set_t* set) { - return pthread_getaffinity_np(thread, sizeof(*set), set); -} - -void cpu_zero(cpu_set_t* set) { - CPU_ZERO(set); -} -void cpu_set(int cpu, cpu_set_t* set) { - CPU_SET(cpu, set); -} -int cpu_isset(int cpu, cpu_set_t* set) { - CPU_ISSET(cpu, set); -} -int cpu_setsize() { - return CPU_SETSIZE; -} diff --git a/litmus/src/nativeInterop/setup.sh b/litmus/src/nativeInterop/setup.sh deleted file mode 100755 index 2a0c1b7..0000000 --- a/litmus/src/nativeInterop/setup.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -INTEROP_FOLDER=$1 - -BINARY_PATH="$INTEROP_FOLDER/kaffinity_gnu.o" -DEF_FILE_PATH="$INTEROP_FOLDER/kaffinity.def" -SOURCE_PATH="$INTEROP_FOLDER/kaffinity_gnu.c" - -gcc "$SOURCE_PATH" -c -o "$BINARY_PATH" -echo "linkerOpts.linux = $BINARY_PATH" > "$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/AffinityManager.kt b/litmus/src/nativeMain/kotlin/komem.litmus/AffinityManager.kt deleted file mode 100644 index 7cab03f..0000000 --- a/litmus/src/nativeMain/kotlin/komem.litmus/AffinityManager.kt +++ /dev/null @@ -1,58 +0,0 @@ -@file:OptIn(kotlin.native.concurrent.ObsoleteWorkersApi::class) - -package komem.litmus - -// TODO: add documentation - -import kotlin.native.concurrent.Worker -import kotlin.random.Random - -interface AffinityManager { - fun setAffinity(w: Worker, cpus: Set) - fun getAffinity(w: Worker): Set - - fun newShiftMap(shift: Int): AffinityMap = object : AffinityMap { - private val cpus: List> - - init { - val tmp = MutableList(cpuCount()) { setOf() } - var i = 0 - repeat(tmp.size) { - tmp[i] = setOf(i) - i = (i + shift) % tmp.size - if (tmp[i].isNotEmpty()) i++ - } - cpus = tmp - } - - override fun allowedCores(threadIndex: Int) = cpus[threadIndex] - } - - fun newRandomMap(random: Random = Random): AffinityMap = object : AffinityMap { - private val cpus = (0.. = listOf( - newShiftMap(1), - newShiftMap(2), - newShiftMap(4), - ) - - fun presetLong(): List = List(cpuCount()) { newShiftMap(it) } + listOf( - object : AffinityMap { - override fun allowedCores(threadIndex: Int) = setOf(0, 1) - }, - object : AffinityMap { - override fun allowedCores(threadIndex: Int) = setOf(1, 2) - }, - object : AffinityMap { - override fun allowedCores(threadIndex: Int) = setOf( - (threadIndex * 2) % cpuCount(), - (threadIndex * 2 + 1) % cpuCount() - ) - } - ) -} - -expect fun getAffinityManager(): AffinityManager? diff --git a/litmus/src/nativeMain/kotlin/komem.litmus/WorkerRunner.kt b/litmus/src/nativeMain/kotlin/komem.litmus/WorkerRunner.kt deleted file mode 100644 index b0373ed..0000000 --- a/litmus/src/nativeMain/kotlin/komem.litmus/WorkerRunner.kt +++ /dev/null @@ -1,60 +0,0 @@ -package komem.litmus - -import komem.litmus.barriers.Barrier -import kotlin.experimental.ExperimentalNativeApi -import kotlin.native.concurrent.ObsoleteWorkersApi -import kotlin.native.concurrent.TransferMode -import kotlin.native.concurrent.Worker - -object WorkerRunner : LitmusRunner() { - - @OptIn(ObsoleteWorkersApi::class, ExperimentalNativeApi::class) - override fun runTest( - params: LitmusRunParams, - test: LitmusTest, - ): LitmusResult { - - data class WorkerContext( - val states: List, - 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 workers = List(test.threadCount) { Worker.start() } - - workers.mapIndexed { threadIndex, worker -> - params.affinityMap?.let { affinityMap -> - getAffinityManager()?.run { - val cpuSet = affinityMap.allowedCores(threadIndex) - setAffinity(worker, cpuSet) - require(getAffinity(worker) == cpuSet) { "affinity setting failed" } - } - } - val workerContext = WorkerContext( - states, - test.threadFunctions[threadIndex], - params.syncPeriod, - barrier, - ) - worker.execute( - TransferMode.SAFE /* ignored */, - { workerContext } - ) { (states, threadFunction, syncPeriod, barrier) -> - for (i in states.indices) { - if (i % syncPeriod == 0) barrier.await() - 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) - } -} 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/LICENSE b/testsuite/LICENSE new file mode 100644 index 0000000..ffd0ab8 --- /dev/null +++ b/testsuite/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 JetBrains Research + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/testsuite/build.gradle.kts b/testsuite/build.gradle.kts new file mode 100644 index 0000000..88f8ddc --- /dev/null +++ b/testsuite/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + kotlin("multiplatform") + id("com.google.devtools.ksp") version "2.0.0-1.0.23" + `java-library` +} + +kotlin { + linuxX64() + linuxArm64() + macosX64() + macosArm64() + mingwX64() + + jvm { + withJava() + } + + sourceSets { + commonMain { + dependencies { + implementation(project(":core")) + } + } + } +} + +// ======== ksp ======== + +val kspTasks = setOf("kspJvm", "kspLinuxX64", "kspMacosX64", "kspMacosArm64", "kspMingwX64") + +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..a2f3d07 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/ArrayVolatile.kt @@ -0,0 +1,40 @@ +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) + } + reset { + arr[0] = 0 + arr[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..01a4a61 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Atomicity.kt @@ -0,0 +1,75 @@ +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) + } + reset { + x = 0 + } + } + + 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) + } + reset { + x = 0L + } + } + + val LongVolatile = litmusTest({ + object : LitmusLOutcome() { + @Volatile + var x = 0L + } + }) { + thread { + x = -1 // signed 0xFFFFFFFF_FFFFFFFF + } + thread { + r1 = x + } + spec { + accept(0) + accept(-1) + } + reset { + x = 0L + } + } +} 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..fad5177 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/Coherence.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.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) + } + reset { + x = 0 + } + } + + data class IntHolder(var x: Int) + + val CSE = litmusTest({ + object : LitmusIIIOutcome() { + var holder1 = IntHolder(0) + var 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) + } + reset { + holder1 = IntHolder(0) + holder2 = holder1 + } + } + +} 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..709e4f4 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/IndependentReadsOfIndependentWrites.kt @@ -0,0 +1,78 @@ +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) + } + reset { + x = 0 + y = 0 + } + } + + // 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) + } + reset { + x = 0 + y = 0 + } + } + +} 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..6d56834 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/LoadBuffering.kt @@ -0,0 +1,114 @@ +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) + } + reset { + x = 0 + y = 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) + } + reset { + x = 0 + y = 0 + } + } + + 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) + } + reset { + x = 0 + y = 0 + } + } + + 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) + } + reset { + x = 0 + y = 0 + } + } + +} 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..c06a7e2 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/MessagePassing.kt @@ -0,0 +1,117 @@ +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) + } + reset { + x = 0 + y = 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) + } + reset { + x = 0 + y = 0 + } + } + + 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) + } + reset { + x = 0 + y = 0 + } + } + + 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) + } + reset { + x = 0 + y = 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..36a10c3 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/StoreBuffering.kt @@ -0,0 +1,70 @@ +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) + } + reset { + x = 0 + y = 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 + } + reset { + x = 0 + y = 0 + } + } + +} 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..c4575aa --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublication.kt @@ -0,0 +1,188 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +import org.jetbrains.litmuskt.autooutcomes.LitmusIOutcome +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 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) + } + reset { + h = null + } + } + + val VolatileAnnotated = litmusTest({ + object : LitmusIOutcome() { + @Volatile + var h: IntHolder? = null + } + }) { + thread { + h = IntHolder() + } + thread { + r1 = h?.x ?: -1 + } + spec { + accept(0) + accept(-1) + } + reset { + h = null + } + } + + val PlainWithConstructor = litmusTest({ + object : LitmusIOutcome() { + var h: IntHolder? = null + } + }) { + thread { + h = IntHolder(x = 1) + } + thread { + r1 = h?.x ?: -1 + } + spec { + accept(1) + accept(-1) + interesting(0) // seeing the default value + } + reset { + h = null + } + } + + val PlainArray = litmusTest({ + object : LitmusIOutcome() { + var arr: Array? = null + } + }) { + thread { + arr = Array(1) { 1 } + } + thread { + r1 = arr?.get(0) ?: -1 + } + spec { + accept(1) + // 0 is the default value for `Int`. However, since Int-s in `Array` are boxed, we don't get to see a 0. + // On JVM, a NullPointerException here is technically valid. Currently, there is no support for exceptions as accepted outcomes. + // On Native, there is no NullPointerException, so we can see a segmentation fault. + interesting(0) + accept(-1) + } + reset { + arr = null + } + } + + val PlainIntArray = litmusTest({ + object : LitmusIOutcome() { + var arr: IntArray? = null + } + }) { + thread { + arr = IntArray(1) { 1 } + } + thread { + r1 = arr?.get(0) ?: -1 + } + spec { + accept(1) + interesting(0) + accept(-1) + } + reset { + arr = null + } + } + + 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 = if (t != null) { + val ref = t.ref + // Despite what IDEA says, this `if` can fail because the default value for a reference is null. + // Also, this `if` is not removed by the compiler even in release mode. + if (ref != null) ref.x else 0 + } else -1 + } + spec { + accept(1) + accept(-1) + // Before the question about full construction guarantee is settled, keep the null outcome + // as forbidden so that it shows up in CI runs. + forbid(0) + } + reset { + h = null + } + } + + private class LeakingIntHolderContext { + var ih: LeakingIntHolder? = null + + inner class LeakingIntHolder { + init { + ih = this + } + + val x: Int = 1 + } + } + + val PlainWithLeakingConstructor = litmusTest({ + object : LitmusIOutcome() { + var ctx = LeakingIntHolderContext() + } + }) { + thread { + ctx.LeakingIntHolder() + } + thread { + r1 = ctx.ih?.x ?: -1 + } + spec { + accept(1) + interesting(0) + accept(-1) + } + reset { + ctx = LeakingIntHolderContext() + } + } + +} 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..bcdc812 --- /dev/null +++ b/testsuite/src/commonMain/kotlin/org/jetbrains/litmuskt/tests/WordTearing.kt @@ -0,0 +1,63 @@ +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) + } + reset { + arr[0] = false + arr[1] = false + } + } + + 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) + } + reset { + for (i in arr.indices) arr[i] = 0 + } + } +} diff --git a/testsuite/src/jvmMain/kotlin/org/jetbrains/litmuskt/UnsafePublicationJvm.kt b/testsuite/src/jvmMain/kotlin/org/jetbrains/litmuskt/UnsafePublicationJvm.kt new file mode 100644 index 0000000..4d4dcb8 --- /dev/null +++ b/testsuite/src/jvmMain/kotlin/org/jetbrains/litmuskt/UnsafePublicationJvm.kt @@ -0,0 +1,30 @@ +package org.jetbrains.litmuskt + +import org.jetbrains.litmuskt.autooutcomes.LitmusIOutcome +import org.jetbrains.litmuskt.autooutcomes.accept +import org.jetbrains.litmuskt.autooutcomes.forbid +import java.util.concurrent.atomic.AtomicIntegerArray + +@LitmusTestContainer +object UnsafePublicationJvm { + val PlainAtomicIntegerArray = litmusTest({ + object : LitmusIOutcome() { + var arr: AtomicIntegerArray? = null + } + }) { + thread { + arr = AtomicIntegerArray(intArrayOf(1)) + } + thread { + r1 = arr?.get(0) ?: -1 + } + spec { + accept(1) + forbid(0) + accept(-1) + } + reset { + arr = null + } + } +} diff --git a/testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublicationNative.kt b/testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublicationNative.kt new file mode 100644 index 0000000..ed2ff61 --- /dev/null +++ b/testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/UnsafePublicationNative.kt @@ -0,0 +1,34 @@ +package org.jetbrains.litmuskt.tests + +import org.jetbrains.litmuskt.LitmusTestContainer +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.AtomicIntArray + +@LitmusTestContainer +object UnsafePublicationNative { + + @OptIn(ExperimentalStdlibApi::class) + val PlainAtomicIntArray = litmusTest({ + object : LitmusIOutcome() { + var arr: AtomicIntArray? = null + } + }) { + thread { + arr = AtomicIntArray(1) { 1 } + } + thread { + r1 = arr?.get(0) ?: -1 + } + spec { + accept(1) + interesting(0) + accept(-1) + } + reset { + arr = null + } + } +} 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..d469bd4 --- /dev/null +++ b/testsuite/src/nativeMain/kotlin/org/jetbrains/litmuskt/tests/WordTearingNative.kt @@ -0,0 +1,38 @@ +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) + } + reset { + bs.clear() + } + } +}