-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Lincheck benchmarks #250
Open
eupp
wants to merge
18
commits into
develop
Choose a base branch
from
lincheck-benchmarks
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Lincheck benchmarks #250
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
c278760
Lincheck statistics gathering and benchmarks infra
eupp cbea9d5
fix gradle build and IDEA imports (with some hack)
eupp 5fdc396
rollback `SequentialIntChannel` renaming
eupp 883c578
minor
eupp a14474d
remove `@Param` annotations in benchmarks
eupp 630d151
Rename IntrinsicLockBenchmark to SynchronizedBenchmark
eupp f2a7c51
add `OptIn`-s + minor fixes after rebase
eupp b1b663d
remove non-linearizable `offer` operation from BufferedChannelBenchmark
eupp 28b56d5
use `compileOnly` dependency on bootstrap in benchmarks
eupp 9880d10
minor grammar fixes in benchmakrs/README.md
eupp b084beb
add comments to `Plots.kt`
eupp 7f3ed25
minor changes in `LincheckRunTracker`
eupp 39bc28d
minor comment
eupp aa2d198
minor
eupp 59f3859
cleanup scenario index tracking logic (scenario minimization)
eupp 3d1fd15
clarify the docs of `warmUpInvocationsCount`
eupp 47ce79b
move `Suppress` to the actual place where it is used
eupp bf35150
fix for Java 8
eupp File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
src/jvm/benchmark/org/jetbrains/kotlinx/lincheck_benchmark/AbstractLincheckBenchmark.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Lincheck | ||
* | ||
* Copyright (C) 2019 - 2023 JetBrains s.r.o. | ||
* | ||
* This Source Code Form is subject to the terms of the | ||
* Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed | ||
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
*/ | ||
|
||
package org.jetbrains.kotlinx.lincheck_benchmark | ||
|
||
import org.jetbrains.kotlinx.lincheck.* | ||
import org.jetbrains.kotlinx.lincheck.strategy.* | ||
import org.jetbrains.kotlinx.lincheck.strategy.managed.modelchecking.ModelCheckingOptions | ||
import org.jetbrains.kotlinx.lincheck.strategy.stress.StressOptions | ||
import kotlin.reflect.KClass | ||
import org.junit.Test | ||
|
||
|
||
abstract class AbstractLincheckBenchmark( | ||
private vararg val expectedFailures: KClass<out LincheckFailure> | ||
) { | ||
|
||
@Test(timeout = TIMEOUT) | ||
ndkoval marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fun benchmarkWithStressStrategy(): Unit = StressOptions().run { | ||
invocationsPerIteration(5_000) | ||
ndkoval marked this conversation as resolved.
Show resolved
Hide resolved
|
||
configure() | ||
runTest() | ||
} | ||
|
||
@Test(timeout = TIMEOUT) | ||
fun benchmarkWithModelCheckingStrategy(): Unit = ModelCheckingOptions().run { | ||
invocationsPerIteration(5_000) | ||
configure() | ||
runTest() | ||
} | ||
|
||
private fun <O : Options<O, *>> O.runTest() { | ||
val statisticsTracker = LincheckStatisticsTracker( | ||
granularity = benchmarksReporter.granularity | ||
) | ||
val klass = this@AbstractLincheckBenchmark::class | ||
val checker = LinChecker(klass.java, this) | ||
val failure = | ||
@Suppress("INVISIBLE_MEMBER") // `checkImpl` API is currently internal in the Lincheck module | ||
checker.checkImpl(customTracker = statisticsTracker) | ||
if (failure == null) { | ||
assert(expectedFailures.isEmpty()) { | ||
"This test should fail, but no error has been occurred (see the logs for details)" | ||
} | ||
} else { | ||
assert(expectedFailures.contains(failure::class)) { | ||
"This test has failed with an unexpected error: \n $failure" | ||
} | ||
} | ||
val statistics = statisticsTracker.toBenchmarkStatistics( | ||
name = klass.simpleName!!.removeSuffix("Benchmark"), | ||
strategy = when (this) { | ||
is StressOptions -> LincheckStrategy.Stress | ||
is ModelCheckingOptions -> LincheckStrategy.ModelChecking | ||
else -> throw IllegalStateException("Unsupported Lincheck strategy") | ||
} | ||
) | ||
benchmarksReporter.registerBenchmark(statistics) | ||
} | ||
|
||
private fun <O : Options<O, *>> O.configure(): Unit = run { | ||
iterations(30) | ||
threads(3) | ||
actorsPerThread(2) | ||
actorsBefore(2) | ||
actorsAfter(2) | ||
minimizeFailedScenario(false) | ||
customize() | ||
} | ||
|
||
internal open fun <O: Options<O, *>> O.customize() {} | ||
|
||
} | ||
|
||
private const val TIMEOUT = 5 * 60 * 1000L // 5 minutes |
130 changes: 130 additions & 0 deletions
130
src/jvm/benchmark/org/jetbrains/kotlinx/lincheck_benchmark/BenchmarkStatistics.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/* | ||
* Lincheck | ||
* | ||
* Copyright (C) 2019 - 2023 JetBrains s.r.o. | ||
* | ||
* This Source Code Form is subject to the terms of the | ||
* Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed | ||
* with this file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
*/ | ||
|
||
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER") | ||
|
||
package org.jetbrains.kotlinx.lincheck_benchmark | ||
|
||
import org.jetbrains.kotlinx.lincheck.* | ||
import kotlinx.serialization.Serializable | ||
import kotlinx.serialization.json.Json | ||
import kotlinx.serialization.json.encodeToStream | ||
import kotlin.time.Duration.Companion.nanoseconds | ||
import kotlin.time.DurationUnit | ||
import java.io.File | ||
|
||
|
||
typealias BenchmarkID = String | ||
|
||
@Serializable | ||
data class BenchmarksReport( | ||
val data: Map<String, BenchmarkStatistics> | ||
) | ||
|
||
@Serializable | ||
data class BenchmarkStatistics( | ||
val name: String, | ||
val strategy: LincheckStrategy, | ||
val runningTimeNano: Long, | ||
val iterationsCount: Int, | ||
val invocationsCount: Int, | ||
val scenariosStatistics: List<ScenarioStatistics>, | ||
val invocationsRunningTimeNano: LongArray, | ||
) | ||
|
||
@Serializable | ||
data class ScenarioStatistics( | ||
val threads: Int, | ||
val operations: Int, | ||
val invocationsCount: Int, | ||
val runningTimeNano: Long, | ||
val invocationAverageTimeNano: Long, | ||
val invocationStandardErrorTimeNano: Long, | ||
) | ||
|
||
val BenchmarksReport.benchmarkIDs: List<BenchmarkID> | ||
get() = data.keys.toList() | ||
|
||
val BenchmarksReport.benchmarkNames: List<String> | ||
get() = data.map { (_, statistics) -> statistics.name }.distinct() | ||
|
||
val BenchmarkStatistics.id: BenchmarkID | ||
get() = "$name-$strategy" | ||
|
||
fun LincheckStatistics.toBenchmarkStatistics(name: String, strategy: LincheckStrategy) = BenchmarkStatistics( | ||
name = name, | ||
strategy = strategy, | ||
runningTimeNano = runningTimeNano, | ||
iterationsCount = iterationsCount, | ||
invocationsCount = invocationsCount, | ||
invocationsRunningTimeNano = iterationsStatistics | ||
.values.map { it.invocationsRunningTimeNano } | ||
.flatten(), | ||
scenariosStatistics = iterationsStatistics | ||
.values.groupBy { (it.scenario.nThreads to it.scenario.parallelExecution[0].size) } | ||
.map { (key, statistics) -> | ||
val (threads, operations) = key | ||
val invocationsRunningTime = statistics | ||
.map { it.invocationsRunningTimeNano } | ||
.flatten() | ||
val invocationsCount = statistics.sumOf { it.invocationsCount } | ||
val runningTimeNano = statistics.sumOf { it.runningTimeNano } | ||
val invocationAverageTimeNano = when { | ||
// handle the case when per-invocation statistics is not gathered | ||
invocationsRunningTime.isEmpty() -> (runningTimeNano.toDouble() / invocationsCount).toLong() | ||
else -> invocationsRunningTime.average().toLong() | ||
} | ||
val invocationStandardErrorTimeNano = when { | ||
// if per-invocation statistics is not gathered we cannot compute standard error | ||
invocationsRunningTime.isEmpty() -> -1L | ||
else -> invocationsRunningTime.standardError().toLong() | ||
} | ||
ScenarioStatistics( | ||
threads = threads, | ||
operations = operations, | ||
invocationsCount = invocationsCount, | ||
runningTimeNano = runningTimeNano, | ||
invocationAverageTimeNano = invocationAverageTimeNano, | ||
invocationStandardErrorTimeNano = invocationStandardErrorTimeNano, | ||
) | ||
} | ||
) | ||
|
||
@OptIn(kotlinx.serialization.ExperimentalSerializationApi::class) | ||
fun BenchmarksReport.saveJson(filename: String) { | ||
val file = File("$filename.json") | ||
file.outputStream().use { outputStream -> | ||
Json.encodeToStream(this, outputStream) | ||
} | ||
} | ||
|
||
// saves the report in simple text format for testing integration with ij-perf dashboards | ||
fun BenchmarksReport.saveTxt(filename: String) { | ||
val text = StringBuilder().apply { | ||
appendReportHeader() | ||
for (benchmarkStatistics in data.values) { | ||
// for ij-perf reports, we currently track only benchmarks overall running time | ||
appendBenchmarkRunningTime(benchmarkStatistics) | ||
} | ||
}.toString() | ||
val file = File("$filename.txt") | ||
file.writeText(text, charset = Charsets.US_ASCII) | ||
} | ||
|
||
private fun StringBuilder.appendReportHeader() { | ||
appendLine("Lincheck benchmarks suite") | ||
} | ||
|
||
private fun StringBuilder.appendBenchmarkRunningTime(benchmarkStatistics: BenchmarkStatistics) { | ||
with(benchmarkStatistics) { | ||
val runningTimeMs = runningTimeNano.nanoseconds.toLong(DurationUnit.MILLISECONDS) | ||
appendLine("${strategy}.${name}.runtime.ms $runningTimeMs") | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please take a look how it is done in the Kotlin coroutines repo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please point out to some specific part or example of gradle scripts you mean, or describe what is the problem with the current code.
The kotlin coroutines build setup is quite large and complicated, what exactly should I look for?
It looks like they use subprojects approach, and benchmarks, in particular, is one of the subprojects.
Is that what you mean? If yes, what are the benefits of using subprojects for benchmarks compared to having them as another source set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making it a subproject makes the build script simpler and modularized, with all the dependencies and hacks necessary for benchmarks being configured for benchmarks only and in a separate file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, it is much shorter 🙃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, will try it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tried the sub-projects approach and it haven't worked out.
The problem is that if we extract the benchmarks into a separate sub-project, then we have to add a dependency from benchmarks sub-module to a the lincheck itself.
But, because the lincheck itself is not a sub-project, but a top-level project, we cannot do (i.e., adding
dependency { implementation(rootProject) }
does not work).It seems that currently Lincheck project structure does not adhere to the standard recommended project structure, where the top-level
build.gradle.kts
simply imports sub-projects and defines common configuration, and individual sub-projects'build.gradle.kts
files define build scripts for sub-components, likelib
,app
,benchmarks
, etc: https://docs.gradle.org/current/userguide/multi_project_builds.htmlThe kotlinx-coroutines project also follows this convention, and thus we cannot simply re-use it in Lincheck.
It defines a
kotlinx-coroutines-core
sub-project with core coroutine library primitives.The
benchmarks
sub-project thendeclares a dependency on the core subproject.
And it looks like other kotlin libraries follow the same approach, where the top-level
build.gradle.kts
is only an "umbrella" script and everything else is split into sub-projects. Most libraries also define "core" sub-project, which is either namedcore
or$library-name
:For us it means that we would have to move the current top-level
build.gradle.kts
file into a new "core" sub-project, which we could name eitherlincheck
,lincheck-core
, orcore
.But this is a major change, which I think we should implement in a separate PR, not as a part of benchmarks PR.
Also this change is likely to produce a lot of merge-conflicts with existing active feature branches, so we should plan it thoughtfully and coordinate with other folks who currently work on various features in Lincheck.
I have very limited knowledge of gradle, so perhaps there is some way to overcome the problems described above. But I do not know how to do without major refactoring of project structure.