Refactor new type of benchmarks and support running on JVM in new mode (#4041)

This commit is contained in:
LepilkinaElena
2020-03-31 12:16:56 +03:00
committed by GitHub
parent c7e4e48447
commit 6c24e9091d
27 changed files with 739 additions and 702 deletions
@@ -0,0 +1,6 @@
package org.jetbrains.kotlin
enum class BenchmarkRepeatingType {
INTERNAL, // Let the benchmark perform warmups and repeats.
EXTERNAL, // Repeat by relaunching benchmark
}
@@ -5,16 +5,21 @@
package org.jetbrains.kotlin
import groovy.lang.Closure
import org.gradle.api.tasks.JavaExec
import org.gradle.api.Task
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
import org.gradle.api.tasks.Input
import org.jetbrains.kotlin.benchmark.LogLevel
import org.jetbrains.kotlin.benchmark.Logger
import org.jetbrains.report.json.*
import java.io.ByteArrayOutputStream
import java.io.File
open class RunJvmTask : JavaExec() {
var outputFileName: String? = null
data class ExecParameters(val warmupCount: Int, val repeatCount: Int,
val filterArgs: List<String>, val filterRegexArgs: List<String>,
val verbose: Boolean, val outputFileName: String?)
open class RunJvmTask: JavaExec() {
@Input
@Option(option = "filter", description = "Benchmarks to run (comma-separated)")
var filter: String = ""
@@ -24,24 +29,97 @@ open class RunJvmTask : JavaExec() {
@Input
@Option(option = "verbose", description = "Verbose mode of running benchmarks")
var verbose: Boolean = false
@Input
var warmupCount: Int = 0
@Input
var repeatCount: Int = 0
@Input
var repeatingType = BenchmarkRepeatingType.INTERNAL
@Input
var outputFileName: String? = null
@JvmOverloads
private fun executeTask(output: java.io.OutputStream? = null) {
val filterArgs = filter.splitCommaSeparatedOption("-f")
val filterRegexArgs = filterRegex.splitCommaSeparatedOption("-fr")
args(filterArgs)
args(filterRegexArgs)
if (verbose) {
private var predefinedArgs: List<String> = emptyList()
private fun executeTask(execParameters: ExecParameters): String {
// Firstly clean arguments.
setArgs(emptyList())
args(predefinedArgs)
args(execParameters.filterArgs)
args(execParameters.filterRegexArgs)
args("-w", execParameters.warmupCount)
args("-r", execParameters.repeatCount)
if (execParameters.verbose) {
args("-v")
}
execParameters.outputFileName?.let { args("-o", outputFileName) }
standardOutput = ByteArrayOutputStream()
super.exec()
return standardOutput.toString()
}
private fun getBenchmarksList(filterArgs: List<String>, filterRegexArgs: List<String>): List<String> {
// Firstly clean arguments.
setArgs(emptyList())
args("list")
standardOutput = ByteArrayOutputStream()
super.exec()
val benchmarks = standardOutput.toString().lines()
val regexes = filterRegexArgs.map { it.toRegex() }
return if (filterArgs.isNotEmpty() || regexes.isNotEmpty()) {
benchmarks.filter { benchmark -> benchmark in filterArgs || regexes.any { it.matches(benchmark) } }
} else benchmarks.filter { !it.isEmpty() }
}
private fun execSeparateBenchmarkRepeatedly(benchmark: String): List<String> {
// Logging with application should be done only in case it controls running benchmarks itself.
// Although it's a responsibility of gradle task.
val logger = if (verbose) Logger(LogLevel.DEBUG) else Logger()
logger.log("Warm up iterations for benchmark $benchmark\n")
for (i in 0.until(warmupCount)) {
executeTask(ExecParameters(0, 1, listOf("-f", benchmark),
emptyList(), false, null))
}
val result = mutableListOf<String>()
logger.log("Running benchmark $benchmark ")
for (i in 0.until(repeatCount)) {
logger.log(".", usePrefix = false)
val benchmarkReport = JsonTreeParser.parse(
executeTask(ExecParameters(0, 1, listOf("-f", benchmark),
emptyList(), false, null)
).removePrefix("[").removeSuffix("]")
).jsonObject
val modifiedBenchmarkReport = JsonObject(HashMap(benchmarkReport.content).apply {
put("repeat", JsonLiteral(i))
put("warmup", JsonLiteral(warmupCount))
})
result.add(modifiedBenchmarkReport.toString())
}
logger.log("\n", usePrefix = false)
return result
}
private fun execBenchmarksRepeatedly(filterArgs: List<String>, filterRegexArgs: List<String>) {
val benchmarksToRun = getBenchmarksList(filterArgs, filterRegexArgs)
val results = benchmarksToRun.flatMap { benchmark ->
execSeparateBenchmarkRepeatedly(benchmark)
}
File(outputFileName).printWriter().use { out ->
out.println("[${results.joinToString(",")}]")
}
}
@TaskAction
override fun exec() {
if (outputFileName != null)
File(outputFileName).outputStream().use { output -> executeTask(output) }
else
executeTask()
assert(outputFileName != null) { "Output file name should be always set" }
predefinedArgs = args ?: emptyList()
val filterArgs = filter.splitCommaSeparatedOption("-f")
val filterRegexArgs = filterRegex.splitCommaSeparatedOption("-fr")
when (repeatingType) {
BenchmarkRepeatingType.INTERNAL -> executeTask(
ExecParameters(warmupCount, repeatCount, filterArgs, filterRegexArgs, verbose, outputFileName)
)
BenchmarkRepeatingType.EXTERNAL -> execBenchmarksRepeatedly(filterArgs, filterRegexArgs)
}
}
}
@@ -7,7 +7,8 @@ package org.jetbrains.kotlin
import org.gradle.api.DefaultTask
import org.gradle.api.Task
import org.jetbrains.kotlin.gradle.tasks.KotlinNativeLink
import org.jetbrains.kotlin.benchmark.Logger
import org.jetbrains.kotlin.benchmark.LogLevel
import org.jetbrains.report.json.*
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.options.Option
@@ -15,15 +16,12 @@ import org.gradle.api.tasks.Input
import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
import kotlin.collections.HashMap
open class RunKotlinNativeTask @Inject constructor(private val linkTask: Task,
private val executable: String,
private val outputFileName: String
) : DefaultTask() {
enum class RepeatingType {
INTERNAL, // Let the benchmark perform warmups and repeats.
EXTERNAL, // Repeat by relaunching benchmark
}
@Input
@Option(option = "filter", description = "Benchmarks to run (comma-separated)")
@@ -39,7 +37,7 @@ open class RunKotlinNativeTask @Inject constructor(private val linkTask: Task,
@Input
var repeatCount: Int = 0
@Input
var repeatingType = RepeatingType.INTERNAL
var repeatingType = BenchmarkRepeatingType.INTERNAL
private val argumentsList = mutableListOf<String>()
@@ -62,7 +60,9 @@ open class RunKotlinNativeTask @Inject constructor(private val linkTask: Task,
it.executable = executable
it.args(argumentsList)
it.args("-f", benchmark)
if (verbose) {
// Logging with application should be done only in case it controls running benchmarks itself.
// Although it's a responsibility of gradle task.
if (verbose && repeatingType == BenchmarkRepeatingType.INTERNAL) {
it.args("-v")
}
it.args("-w", warmupCount.toString())
@@ -73,11 +73,15 @@ open class RunKotlinNativeTask @Inject constructor(private val linkTask: Task,
}
private fun execBenchmarkRepeatedly(benchmark: String, warmupCount: Int, repeatCount: Int) : List<String> {
for (i in 0..warmupCount) {
val logger = if (verbose) Logger(LogLevel.DEBUG) else Logger()
logger.log("Warm up iterations for benchmark $benchmark\n")
for (i in 0.until(warmupCount)) {
execBenchmarkOnce(benchmark, 0, 1)
}
val result = mutableListOf<String>()
for (i in 0..repeatCount) {
logger.log("Running benchmark $benchmark ")
for (i in 0.until(repeatCount)) {
logger.log(".", usePrefix = false)
val benchmarkReport = JsonTreeParser.parse(execBenchmarkOnce(benchmark, 0, 1)).jsonObject
val modifiedBenchmarkReport = JsonObject(HashMap(benchmarkReport.content).apply {
put("repeat", JsonLiteral(i))
@@ -85,6 +89,7 @@ open class RunKotlinNativeTask @Inject constructor(private val linkTask: Task,
})
result.add(modifiedBenchmarkReport.toString())
}
logger.log("\n", usePrefix = false)
return result
}
@@ -106,8 +111,8 @@ open class RunKotlinNativeTask @Inject constructor(private val linkTask: Task,
val results = benchmarksToRun.flatMap { benchmark ->
when (repeatingType) {
RepeatingType.INTERNAL -> listOf(execBenchmarkOnce(benchmark, warmupCount, repeatCount))
RepeatingType.EXTERNAL -> execBenchmarkRepeatedly(benchmark, warmupCount, repeatCount)
BenchmarkRepeatingType.INTERNAL -> listOf(execBenchmarkOnce(benchmark, warmupCount, repeatCount))
BenchmarkRepeatingType.EXTERNAL -> execBenchmarkRepeatedly(benchmark, warmupCount, repeatCount)
}
}
@@ -0,0 +1,25 @@
package org.jetbrains.kotlin.benchmark
import java.text.SimpleDateFormat
import java.util.*
enum class LogLevel { DEBUG, OFF }
class Logger(val level: LogLevel = LogLevel.OFF) {
private fun printStderr(message: String) {
System.err.print(message)
}
private fun currentTime(): String =
SimpleDateFormat("HH:mm:ss").format(Date())
fun log(message: String, messageLevel: LogLevel = LogLevel.DEBUG, usePrefix: Boolean = true) {
if (messageLevel == level) {
if (usePrefix) {
printStderr("[$level][${currentTime()}] $message")
} else {
printStderr("$message")
}
}
}
}
@@ -70,7 +70,7 @@ open class BenchmarkExtension @Inject constructor(val project: Project) {
var linkerOpts: Collection<String> = emptyList()
var compilerOpts: List<String> = emptyList()
var buildType: NativeBuildType = NativeBuildType.RELEASE
var repeatingType: RunKotlinNativeTask.RepeatingType = RunKotlinNativeTask.RepeatingType.INTERNAL
var repeatingType: BenchmarkRepeatingType = BenchmarkRepeatingType.INTERNAL
val dependencies: BenchmarkDependencies = BenchmarkDependencies()
@@ -8,7 +8,6 @@ import org.jetbrains.kotlin.*
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.mpp.Executable
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
import org.jetbrains.kotlin.konan.target.HostManager
import javax.inject.Inject
import kotlin.reflect.KClass
@@ -76,12 +75,11 @@ open class KotlinNativeBenchmarkingPlugin: BenchmarkingPlugin() {
// Specify settings configured by a user in the benchmark extension.
afterEvaluate {
task.args(
"-w", jvmWarmup,
"-r", attempts,
"-o", buildDir.resolve(jvmBenchResults),
"-p", "${benchmark.applicationName}::"
)
task.args("-p", "${benchmark.applicationName}::")
task.warmupCount = jvmWarmup
task.repeatCount = attempts
task.outputFileName = buildDir.resolve(jvmBenchResults).absolutePath
task.repeatingType = benchmark.repeatingType
}
}
}
@@ -19,24 +19,6 @@ package org.jetbrains.ring
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater
import java.util.concurrent.locks.ReentrantLock
//-----------------------------------------------------------------------------//
actual class Random actual constructor() {
actual companion object {
actual var seedInt = 0
actual fun nextInt(boundary: Int): Int {
seedInt = (3 * seedInt + 11) % boundary
return seedInt
}
actual var seedDouble: Double = 0.1
actual fun nextDouble(boundary: Double): Double {
seedDouble = (7.0 * seedDouble + 7.0) % boundary
return seedDouble
}
}
}
internal var interceptor: AtomicOperationInterceptor = DefaultInterceptor
private set
private val interceptorLock = ReentrantLock()
@@ -20,25 +20,6 @@ import kotlin.native.concurrent.FreezableAtomicReference as KAtomicRef
import kotlin.native.concurrent.isFrozen
import kotlin.native.concurrent.freeze
//-----------------------------------------------------------------------------//
actual class Random actual constructor() {
@kotlin.native.ThreadLocal
actual companion object {
actual var seedInt = 0
actual fun nextInt(boundary: Int): Int {
seedInt = (3 * seedInt + 11) % boundary
return seedInt
}
actual var seedDouble: Double = 0.1
actual fun nextDouble(boundary: Double): Double {
seedDouble = (7.0 * seedDouble + 7.0) % boundary
return seedDouble
}
}
}
public actual class AtomicRef<T> constructor(@PublishedApi internal val a: KAtomicRef<T>) {
public actual inline var value: T
get() = a.value
@@ -15,6 +15,7 @@
*/
package org.jetbrains.ring
import org.jetbrains.benchmarksLauncher.Random
/**
* Created by Mikhail.Glukhikh on 10/03/2015.
@@ -17,6 +17,7 @@
package org.jetbrains.ring
import org.jetbrains.benchmarksLauncher.Blackhole
import org.jetbrains.benchmarksLauncher.Random
open class ElvisBenchmark {
@@ -16,6 +16,8 @@
package org.jetbrains.ring
import org.jetbrains.benchmarksLauncher.Random
var globalAddendum = 0
open class LambdaBenchmark {
@@ -5,6 +5,8 @@
package org.jetbrains.ring
import org.jetbrains.benchmarksLauncher.Random
class ChunkBuffer(var readPosition: Int, var writePosition: Int = readPosition + Random.nextInt(50)) {
private val nextRef: AtomicRef<ChunkBuffer?> = atomic(null)
@@ -16,7 +16,8 @@
package org.jetbrains.ring
import org.jetbrains.ring.BENCHMARK_SIZE
import org.jetbrains.benchmarksLauncher.Blackhole
import org.jetbrains.benchmarksLauncher.Random
/**
* This class emulates matrix behaviour using a hash map as its implementation
@@ -6,6 +6,7 @@
package org.jetbrains.ring
import org.jetbrains.benchmarksLauncher.Blackhole
import org.jetbrains.benchmarksLauncher.Random
private object A {
val a = Random.nextInt(100)
@@ -16,6 +16,8 @@
package org.jetbrains.ring
import org.jetbrains.benchmarksLauncher.Random
open class StringBenchmark {
private var _data: ArrayList<String>? = null
val data: ArrayList<String>
@@ -17,6 +17,7 @@
package org.jetbrains.ring
import org.jetbrains.benchmarksLauncher.Blackhole
import org.jetbrains.benchmarksLauncher.Random
val SPARSE_SWITCH_CASES = intArrayOf(11, 29, 47, 71, 103,
149, 175, 227, 263, 307,
@@ -18,16 +18,6 @@ package org.jetbrains.ring
const val BENCHMARK_SIZE = 10000
expect class Random() {
companion object {
var seedInt: Int
fun nextInt(boundary: Int = 100): Int
var seedDouble: Double
fun nextDouble(boundary: Double = 100.0): Double
}
}
expect class AtomicRef<T> {
/**
* Reading/writing this property maps to read/write of volatile variable.
@@ -56,3 +56,19 @@ actual class Blackhole {
}
}
actual class Random actual constructor() {
actual companion object {
actual var seedInt = 0
actual fun nextInt(boundary: Int): Int {
seedInt = (3 * seedInt + 11) % boundary
return seedInt
}
actual var seedDouble: Double = 0.1
actual fun nextDouble(boundary: Double): Double {
seedDouble = (7.0 * seedDouble + 7.0) % boundary
return seedDouble
}
}
}
@@ -59,4 +59,21 @@ actual class Blackhole {
consumer += value.hashCode()
}
}
}
actual class Random actual constructor() {
@kotlin.native.ThreadLocal
actual companion object {
actual var seedInt = 0
actual fun nextInt(boundary: Int): Int {
seedInt = (3 * seedInt + 11) % boundary
return seedInt
}
actual var seedDouble: Double = 0.1
actual fun nextDouble(boundary: Double): Double {
seedDouble = (7.0 * seedDouble + 7.0) % boundary
return seedDouble
}
}
}
@@ -18,23 +18,25 @@ package org.jetbrains.benchmarksLauncher
import org.jetbrains.report.BenchmarkResult
interface AbstractBenchmarkEntry
interface AbstractBenchmarkEntry {
open val useAutoEvaluatedNumberOfMeasure: Boolean
}
class BenchmarkEntryWithInit(val ctor: ()->Any, val lambda: (Any) -> Any?): AbstractBenchmarkEntry {
companion object {
inline fun <T: Any> create(noinline ctor: ()->T, crossinline lambda: T.() -> Any?) = BenchmarkEntryWithInit(ctor) { (it as T).lambda() }
}
override val useAutoEvaluatedNumberOfMeasure: Boolean = true
}
class BenchmarkEntry(val lambda: () -> Any?) : AbstractBenchmarkEntry
open class BenchmarkEntry(val lambda: () -> Any?) : AbstractBenchmarkEntry {
override val useAutoEvaluatedNumberOfMeasure: Boolean = true
}
// Controls warmup and repeats manually.
data class BenchmarkManualResult(
val status: BenchmarkResult.Status,
val value: Any?,
val warmupCount: Int,
val durationsNs: List<Double>)
class BenchmarkEntryManual(val lambda: () -> BenchmarkManualResult) : AbstractBenchmarkEntry
class BenchmarkEntryManual(lambda: () -> Any?) : BenchmarkEntry(lambda) {
override val useAutoEvaluatedNumberOfMeasure: Boolean = false
}
class BenchmarksCollection(private val benchmarks: MutableMap<String, AbstractBenchmarkEntry> = mutableMapOf()) :
MutableMap<String, AbstractBenchmarkEntry> by benchmarks
@@ -35,4 +35,14 @@ expect class Blackhole {
var consumer: Int
fun consume(value: Any)
}
}
expect class Random() {
companion object {
var seedInt: Int
fun nextInt(boundary: Int = 100): Int
var seedDouble: Double
fun nextDouble(boundary: Double = 100.0): Double
}
}
@@ -67,17 +67,17 @@ abstract class Launcher {
}
}
fun runBenchmarkRepeatedly(logger: Logger,
numWarmIterations: Int,
numberOfAttempts: Int,
name: String,
recordMeasurement: (RecordTimeMeasurement) -> Unit,
benchmarkInstance: Any?,
benchmark: AbstractBenchmarkEntry) {
fun runBenchmark(logger: Logger,
numWarmIterations: Int,
numberOfAttempts: Int,
name: String,
recordMeasurement: (RecordTimeMeasurement) -> Unit,
benchmark: AbstractBenchmarkEntry) {
val benchmarkInstance = (benchmark as? BenchmarkEntryWithInit)?.ctor?.invoke()
logger.log("Warm up iterations for benchmark $name\n")
runBenchmark(benchmarkInstance, benchmark, numWarmIterations)
var autoEvaluatedNumberOfMeasureIteration = 1
while (true) {
while (true && benchmark.useAutoEvaluatedNumberOfMeasure) {
var j = autoEvaluatedNumberOfMeasureIteration
val time = runBenchmark(benchmarkInstance, benchmark, j)
if (time >= 100L * 1_000_000) // 100ms
@@ -85,7 +85,7 @@ abstract class Launcher {
autoEvaluatedNumberOfMeasureIteration *= 2
}
logger.log("Running benchmark $name ")
for (k in 0..numberOfAttempts) {
for (k in 0.until(numberOfAttempts)) {
logger.log(".", usePrefix = false)
var i = autoEvaluatedNumberOfMeasureIteration
val time = runBenchmark(benchmarkInstance, benchmark, i)
@@ -96,43 +96,6 @@ abstract class Launcher {
logger.log("\n", usePrefix = false)
}
fun runBenchmark(logger: Logger,
numWarmIterations: Int,
numberOfAttempts: Int,
name: String,
recordMeasurement: (RecordTimeMeasurement) -> Unit,
benchmark: AbstractBenchmarkEntry) {
when (benchmark) {
is BenchmarkEntryWithInit -> {
val benchmarkInstance = benchmark.ctor?.invoke()
runBenchmarkRepeatedly(logger,
numWarmIterations,
numberOfAttempts,
name,
recordMeasurement,
benchmarkInstance,
benchmark)
}
is BenchmarkEntry -> {
runBenchmarkRepeatedly(logger,
numWarmIterations,
numberOfAttempts,
name,
recordMeasurement,
null,
benchmark)
}
is BenchmarkEntryManual -> {
logger.log("Running manual benchmark $name")
val result = benchmark.lambda()
for ((i, durationNs) in result.durationsNs.withIndex()) {
recordMeasurement(RecordTimeMeasurement(result.status, i, result.warmupCount, durationNs))
}
}
else -> error("unknown benchmark type $benchmark")
}
}
fun launch(numWarmIterations: Int,
numberOfAttempts: Int,
prefix: String = "",
@@ -166,7 +129,10 @@ abstract class Launcher {
runBenchmark(logger, numWarmIterations, numberOfAttempts, name, recordMeasurement, benchmark)
} catch (e: Throwable) {
printStderr("Failure while running benchmark $name: $e\n")
throw e
benchmarkResults.add(BenchmarkResult(
"$prefix$name", BenchmarkResult.Status.FAILED, 0.0,
BenchmarkResult.Metric.EXECUTION_TIME, 0.0, numberOfAttempts, numWarmIterations)
)
}
}
return benchmarkResults
+6 -5
View File
@@ -1,5 +1,6 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
import org.jetbrains.kotlin.RunKotlinNativeTask
import org.jetbrains.kotlin.BenchmarkRepeatingType
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
@@ -15,12 +16,12 @@ val defaultBuildType = NativeBuildType.RELEASE
benchmark {
applicationName = "Startup"
commonSrcDirs = listOf("../../tools/benchmarks/shared/src", "src/main/kotlin", "../shared/src/main/kotlin")
jvmSrcDirs = listOf("src/main/kotlin-jvm", "../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/mingw")
posixSrcDirs = listOf("src/main/kotlin-native", "../shared/src/main/kotlin-native/posix")
jvmSrcDirs = listOf("../shared/src/main/kotlin-jvm")
nativeSrcDirs = listOf("../shared/src/main/kotlin-native/common")
mingwSrcDirs = listOf("../shared/src/main/kotlin-native/mingw")
posixSrcDirs = listOf("../shared/src/main/kotlin-native/posix")
buildType = (findProperty("nativeBuildType") as String?)?.let { NativeBuildType.valueOf(it) } ?: defaultBuildType
repeatingType = RunKotlinNativeTask.RepeatingType.EXTERNAL
repeatingType = BenchmarkRepeatingType.EXTERNAL
dependencies.common(project(":endorsedLibraries:kotlinx.cli"))
}
@@ -1,16 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.startup
actual class Random actual constructor() {
actual companion object {
actual var seedInt = 0
actual fun nextInt(boundary: Int): Int {
seedInt = (3 * seedInt + 11) % boundary
return seedInt
}
}
}
@@ -1,17 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.startup
actual class Random actual constructor() {
@kotlin.native.ThreadLocal
actual companion object {
actual var seedInt = 0
actual fun nextInt(boundary: Int): Int {
seedInt = (3 * seedInt + 11) % boundary
return seedInt
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,13 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the LICENSE file.
*/
package org.jetbrains.startup
expect class Random() {
companion object {
var seedInt: Int
fun nextInt(boundary: Int = 100): Int
}
}