added Multiple Inheritance
This commit is contained in:
1 parent
882df67909
commit
aeeec2d417
33 files changed
+3302
-130
No files matched your search
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.sergeych.lyng.PerfFlags
|
||||
import java.io.File
|
||||
import java.lang.management.GarbageCollectorMXBean
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.random.Random
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
class BookAllocationProfileTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/book_alloc_profile.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Book allocation/time profiling (JVM)\n")
|
||||
f.appendText("[DEBUG_LOG] All sizes in bytes; time in ns (lower is better).\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
// Optional STDERR filter to hide benign warnings during profiling runs
|
||||
private inline fun <T> withFilteredStderr(vararg suppressContains: String, block: () -> T): T {
|
||||
val orig = System.err
|
||||
val filtering = java.io.PrintStream(object : java.io.OutputStream() {
|
||||
private val buf = StringBuilder()
|
||||
override fun write(b: Int) {
|
||||
if (b == '\n'.code) {
|
||||
val line = buf.toString()
|
||||
val suppress = suppressContains.any { line.contains(it) }
|
||||
if (!suppress) orig.println(line)
|
||||
buf.setLength(0)
|
||||
} else buf.append(b.toChar())
|
||||
}
|
||||
})
|
||||
return try {
|
||||
System.setErr(filtering)
|
||||
block()
|
||||
} finally {
|
||||
System.setErr(orig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forceGc() {
|
||||
// Best-effort GC to stabilize measurements
|
||||
repeat(3) {
|
||||
System.gc()
|
||||
try { Thread.sleep(25) } catch (_: InterruptedException) {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun usedHeap(): Long {
|
||||
val mem = ManagementFactory.getMemoryMXBean().heapMemoryUsage
|
||||
return mem.used
|
||||
}
|
||||
|
||||
private suspend fun runBooksOnce(): Unit = runBlocking {
|
||||
// Mirror BookTest set
|
||||
runDocTests("../docs/tutorial.md")
|
||||
runDocTests("../docs/math.md")
|
||||
runDocTests("../docs/advanced_topics.md")
|
||||
runDocTests("../docs/OOP.md")
|
||||
runDocTests("../docs/Real.md")
|
||||
runDocTests("../docs/List.md")
|
||||
runDocTests("../docs/Range.md")
|
||||
runDocTests("../docs/Set.md")
|
||||
runDocTests("../docs/Map.md")
|
||||
runDocTests("../docs/Buffer.md")
|
||||
// Samples folder, bookMode=true
|
||||
for (bt in Files.list(Paths.get("../docs/samples")).toList()) {
|
||||
if (bt.extension == "md") runDocTests(bt.toString(), bookMode = true)
|
||||
}
|
||||
runDocTests("../docs/declaring_arguments.md")
|
||||
runDocTests("../docs/exceptions_handling.md")
|
||||
runDocTests("../docs/time.md")
|
||||
runDocTests("../docs/parallelism.md")
|
||||
runDocTests("../docs/RingBuffer.md")
|
||||
runDocTests("../docs/Iterable.md")
|
||||
}
|
||||
|
||||
private data class ProfileResult(val timeNs: Long, val allocBytes: Long)
|
||||
|
||||
private suspend fun profileRun(): ProfileResult {
|
||||
forceGc()
|
||||
val before = usedHeap()
|
||||
val elapsed = measureNanoTime {
|
||||
withFilteredStderr("ScriptFlowIsNoMoreCollected") {
|
||||
runBooksOnce()
|
||||
}
|
||||
}
|
||||
forceGc()
|
||||
val after = usedHeap()
|
||||
val alloc = (after - before).coerceAtLeast(0)
|
||||
return ProfileResult(elapsed, alloc)
|
||||
}
|
||||
|
||||
private data class GcSnapshot(val count: Long, val timeMs: Long)
|
||||
private fun gcSnapshot(): GcSnapshot {
|
||||
var c = 0L
|
||||
var t = 0L
|
||||
for (gc: GarbageCollectorMXBean in ManagementFactory.getGarbageCollectorMXBeans()) {
|
||||
c += (gc.collectionCount.takeIf { it >= 0 } ?: 0)
|
||||
t += (gc.collectionTime.takeIf { it >= 0 } ?: 0)
|
||||
}
|
||||
return GcSnapshot(c, t)
|
||||
}
|
||||
|
||||
// --- Optional JFR support via reflection (works only on JDKs with Flight Recorder) ---
|
||||
private class JfrHandle(val rec: Any, val dump: (File) -> Unit, val stop: () -> Unit)
|
||||
|
||||
private fun jfrStartIfRequested(name: String): JfrHandle? {
|
||||
val enabled = System.getProperty("lyng.jfr")?.toBoolean() == true
|
||||
if (!enabled) return null
|
||||
return try {
|
||||
val recCl = Class.forName("jdk.jfr.Recording")
|
||||
val ctor = recCl.getDeclaredConstructor()
|
||||
val rec = ctor.newInstance()
|
||||
val setName = recCl.methods.firstOrNull { it.name == "setName" && it.parameterTypes.size == 1 }
|
||||
setName?.invoke(rec, "Lyng-$name")
|
||||
val start = recCl.methods.first { it.name == "start" && it.parameterTypes.isEmpty() }
|
||||
start.invoke(rec)
|
||||
val stop = recCl.methods.first { it.name == "stop" && it.parameterTypes.isEmpty() }
|
||||
val dump = recCl.methods.firstOrNull { it.name == "dump" && it.parameterTypes.size == 1 }
|
||||
val dumper: (File) -> Unit = if (dump != null) {
|
||||
{ f -> dump.invoke(rec, f.toPath()) }
|
||||
} else {
|
||||
{ _ -> }
|
||||
}
|
||||
JfrHandle(rec, dumper) { stop.invoke(rec) }
|
||||
} catch (e: Throwable) {
|
||||
// JFR requested but not available; note once via stdout and proceed without it
|
||||
try {
|
||||
println("[DEBUG_LOG] JFR not available on this JVM; run with Oracle/OpenJDK 11+ to enable -Dlyng.jfr=true")
|
||||
} catch (_: Throwable) {}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun intProp(name: String, def: Int): Int =
|
||||
System.getProperty(name)?.toIntOrNull() ?: def
|
||||
|
||||
private fun boolProp(name: String, def: Boolean): Boolean =
|
||||
System.getProperty(name)?.toBoolean() ?: def
|
||||
|
||||
private data class FlagSnapshot(
|
||||
val RVAL_FASTPATH: Boolean,
|
||||
val PRIMITIVE_FASTOPS: Boolean,
|
||||
val ARG_BUILDER: Boolean,
|
||||
val ARG_SMALL_ARITY_12: Boolean,
|
||||
val FIELD_PIC: Boolean,
|
||||
val METHOD_PIC: Boolean,
|
||||
val FIELD_PIC_SIZE_4: Boolean,
|
||||
val METHOD_PIC_SIZE_4: Boolean,
|
||||
val INDEX_PIC: Boolean,
|
||||
val INDEX_PIC_SIZE_4: Boolean,
|
||||
val SCOPE_POOL: Boolean,
|
||||
val PIC_DEBUG_COUNTERS: Boolean,
|
||||
) {
|
||||
fun restore() {
|
||||
PerfFlags.RVAL_FASTPATH = RVAL_FASTPATH
|
||||
PerfFlags.PRIMITIVE_FASTOPS = PRIMITIVE_FASTOPS
|
||||
PerfFlags.ARG_BUILDER = ARG_BUILDER
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = ARG_SMALL_ARITY_12
|
||||
PerfFlags.FIELD_PIC = FIELD_PIC
|
||||
PerfFlags.METHOD_PIC = METHOD_PIC
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = FIELD_PIC_SIZE_4
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = METHOD_PIC_SIZE_4
|
||||
PerfFlags.INDEX_PIC = INDEX_PIC
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = INDEX_PIC_SIZE_4
|
||||
PerfFlags.SCOPE_POOL = SCOPE_POOL
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = PIC_DEBUG_COUNTERS
|
||||
}
|
||||
}
|
||||
|
||||
private fun snapshotFlags() = FlagSnapshot(
|
||||
RVAL_FASTPATH = PerfFlags.RVAL_FASTPATH,
|
||||
PRIMITIVE_FASTOPS = PerfFlags.PRIMITIVE_FASTOPS,
|
||||
ARG_BUILDER = PerfFlags.ARG_BUILDER,
|
||||
ARG_SMALL_ARITY_12 = PerfFlags.ARG_SMALL_ARITY_12,
|
||||
FIELD_PIC = PerfFlags.FIELD_PIC,
|
||||
METHOD_PIC = PerfFlags.METHOD_PIC,
|
||||
FIELD_PIC_SIZE_4 = PerfFlags.FIELD_PIC_SIZE_4,
|
||||
METHOD_PIC_SIZE_4 = PerfFlags.METHOD_PIC_SIZE_4,
|
||||
INDEX_PIC = PerfFlags.INDEX_PIC,
|
||||
INDEX_PIC_SIZE_4 = PerfFlags.INDEX_PIC_SIZE_4,
|
||||
SCOPE_POOL = PerfFlags.SCOPE_POOL,
|
||||
PIC_DEBUG_COUNTERS = PerfFlags.PIC_DEBUG_COUNTERS,
|
||||
)
|
||||
|
||||
private fun median(values: List<Long>): Long {
|
||||
if (values.isEmpty()) return 0
|
||||
val s = values.sorted()
|
||||
val mid = s.size / 2
|
||||
return if (s.size % 2 == 1) s[mid] else ((s[mid - 1] + s[mid]) / 2)
|
||||
}
|
||||
|
||||
private suspend fun runScenario(
|
||||
name: String,
|
||||
prepare: () -> Unit,
|
||||
repeats: Int = 3,
|
||||
out: (String) -> Unit
|
||||
): ProfileResult {
|
||||
val warmup = intProp("lyng.profile.warmup", 1)
|
||||
val reps = intProp("lyng.profile.repeats", repeats)
|
||||
// JFR
|
||||
val jfr = jfrStartIfRequested(name)
|
||||
if (System.getProperty("lyng.jfr")?.toBoolean() == true && jfr == null) {
|
||||
out("[DEBUG_LOG] JFR: requested but not available on this JVM")
|
||||
}
|
||||
// Warm-up before GC snapshot (some profilers prefer this)
|
||||
prepare()
|
||||
repeat(warmup) { profileRun() }
|
||||
// GC baseline
|
||||
val gc0 = gcSnapshot()
|
||||
val times = ArrayList<Long>(repeats)
|
||||
val allocs = ArrayList<Long>(repeats)
|
||||
repeat(reps) {
|
||||
val r = profileRun()
|
||||
times += r.timeNs
|
||||
allocs += r.allocBytes
|
||||
}
|
||||
val pr = ProfileResult(median(times), median(allocs))
|
||||
val gc1 = gcSnapshot()
|
||||
val gcCountDelta = (gc1.count - gc0.count).coerceAtLeast(0)
|
||||
val gcTimeDelta = (gc1.timeMs - gc0.timeMs).coerceAtLeast(0)
|
||||
out("[DEBUG_LOG] time=${pr.timeNs} ns, alloc=${pr.allocBytes} B (median of ${reps}), GC(count=${gcCountDelta}, timeMs=${gcTimeDelta})")
|
||||
// Stop and dump JFR if enabled
|
||||
if (jfr != null) {
|
||||
try {
|
||||
jfr.stop()
|
||||
val dumpFile = File("lynglib/build/jfr_${name}.jfr")
|
||||
jfr.dump(dumpFile)
|
||||
out("[DEBUG_LOG] JFR dumped: ${dumpFile.path}")
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
return pr
|
||||
}
|
||||
|
||||
@Test
|
||||
fun profile_books_allocations_and_time() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
fun log(s: String) = appendLine(f, s)
|
||||
|
||||
val saved = snapshotFlags()
|
||||
try {
|
||||
data class Scenario(val label: String, val title: String, val prep: () -> Unit)
|
||||
val scenarios = mutableListOf<Scenario>()
|
||||
// Baseline A
|
||||
scenarios += Scenario("A", "JVM defaults") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
}
|
||||
// Most flags OFF B
|
||||
scenarios += Scenario("B", "most perf flags OFF") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
PerfFlags.RVAL_FASTPATH = false
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = false
|
||||
PerfFlags.FIELD_PIC = false
|
||||
PerfFlags.METHOD_PIC = false
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = false
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = false
|
||||
PerfFlags.INDEX_PIC = false
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
}
|
||||
// Defaults with INDEX_PIC size 2 C
|
||||
scenarios += Scenario("C", "defaults except INDEX_PIC_SIZE_4=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false
|
||||
PerfFlags.INDEX_PIC = true; PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
}
|
||||
|
||||
// One-flag toggles relative to A
|
||||
scenarios += Scenario("D", "A with RVAL_FASTPATH=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.RVAL_FASTPATH = false
|
||||
}
|
||||
scenarios += Scenario("E", "A with PRIMITIVE_FASTOPS=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
}
|
||||
scenarios += Scenario("F", "A with INDEX_PIC=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.INDEX_PIC = false
|
||||
}
|
||||
scenarios += Scenario("G", "A with SCOPE_POOL=false") {
|
||||
saved.restore(); PerfFlags.PIC_DEBUG_COUNTERS = false; PerfFlags.SCOPE_POOL = false
|
||||
}
|
||||
|
||||
val shuffle = boolProp("lyng.profile.shuffle", true)
|
||||
val order = if (shuffle) scenarios.shuffled(Random(System.nanoTime())) else scenarios
|
||||
|
||||
val results = mutableMapOf<String, ProfileResult>()
|
||||
for (sc in order) {
|
||||
log("[DEBUG_LOG] Scenario ${sc.label}: ${sc.title}")
|
||||
results[sc.label] = runScenario(sc.label, prepare = sc.prep, out = ::log)
|
||||
}
|
||||
|
||||
// Summary vs A if measured
|
||||
val a = results["A"]
|
||||
if (a != null) {
|
||||
log("[DEBUG_LOG] Summary deltas vs A (medians):")
|
||||
fun deltaLine(name: String, r: ProfileResult) = "[DEBUG_LOG] ${name} - A: time=${r.timeNs - a.timeNs} ns, alloc=${r.allocBytes - a.allocBytes} B"
|
||||
listOf("B","C","D","E","F","G").forEach { k ->
|
||||
results[k]?.let { r -> log(deltaLine(k, r)) }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
saved.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal runBlocking bridge to avoid extra test deps here
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
class CallArgPipelineABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/call_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Call/Arg pipeline A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) {
|
||||
f.appendText(s + "\n")
|
||||
}
|
||||
|
||||
private suspend fun buildScriptForCalls(arity: Int, iters: Int): Script {
|
||||
val argsDecl = (0 until arity).joinToString(",") { "a$it" }
|
||||
val argsUse = (0 until arity).joinToString(" + ") { "a$it" }.ifEmpty { "0" }
|
||||
val callArgs = (0 until arity).joinToString(",") { (it + 1).toString() }
|
||||
val src = """
|
||||
var sum = 0
|
||||
fun f($argsDecl) { $argsUse }
|
||||
for(i in 0..${iters - 1}) {
|
||||
sum += f($callArgs)
|
||||
}
|
||||
sum
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<calls-$arity>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun benchCallsOnce(arity: Int, iters: Int): Long {
|
||||
val script = buildScriptForCalls(arity, iters)
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime {
|
||||
result = script.execute(scope)
|
||||
}
|
||||
// Basic correctness check so JIT doesn’t elide
|
||||
val expected = (0 until iters).fold(0L) { acc, _ ->
|
||||
(acc + (1L + 2L + 3L + 4L + 5L + 6L + 7L + 8L).let { if (arity <= 8) it - (8 - arity) * 0L else it })
|
||||
}
|
||||
// We only rely that it runs; avoid strict equals as function may compute differently for arities < 8
|
||||
if (result !is ObjInt) {
|
||||
// ensure use to prevent DCE
|
||||
println("[DEBUG_LOG] Result class=${result?.javaClass?.simpleName}")
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
private suspend fun benchOptionalCallShortCircuit(iters: Int): Long {
|
||||
val src = """
|
||||
var side = 0
|
||||
fun inc() { side += 1 }
|
||||
var o = null
|
||||
for(i in 0..${iters - 1}) {
|
||||
o?.foo(inc())
|
||||
}
|
||||
side
|
||||
""".trimIndent()
|
||||
val script = Compiler.compile(Source("<opt-call>", src), Script.defaultImportManager)
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
// Ensure short-circuit actually happened
|
||||
require((result as? ObjInt)?.value == 0L) { "optional-call short-circuit failed; side=${(result as? ObjInt)?.value}" }
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_call_pipeline() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedArgBuilder = PerfFlags.ARG_BUILDER
|
||||
val savedScopePool = PerfFlags.SCOPE_POOL
|
||||
|
||||
try {
|
||||
val iters = 50_000
|
||||
val aritiesBase = listOf(0, 1, 2, 3, 4, 5, 6, 7, 8)
|
||||
|
||||
// A/B for ARG_BUILDER (0..8)
|
||||
PerfFlags.ARG_BUILDER = false
|
||||
val offTimes = mutableListOf<Long>()
|
||||
for (a in aritiesBase) offTimes += benchCallsOnce(a, iters)
|
||||
PerfFlags.ARG_BUILDER = true
|
||||
val onTimes = mutableListOf<Long>()
|
||||
for (a in aritiesBase) onTimes += benchCallsOnce(a, iters)
|
||||
|
||||
appendLine(f, "[DEBUG_LOG] ARG_BUILDER A/B (iters=$iters):")
|
||||
aritiesBase.forEachIndexed { idx, a ->
|
||||
appendLine(f, "[DEBUG_LOG] arity=$a OFF=${offTimes[idx]} ns, ON=${onTimes[idx]} ns, delta=${offTimes[idx] - onTimes[idx]} ns")
|
||||
}
|
||||
|
||||
// A/B for ARG_SMALL_ARITY_12 (9..12)
|
||||
val aritiesExtended = listOf(9, 10, 11, 12)
|
||||
val savedSmall = PerfFlags.ARG_SMALL_ARITY_12
|
||||
try {
|
||||
PerfFlags.ARG_BUILDER = true // base builder on
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = false
|
||||
val offExt = mutableListOf<Long>()
|
||||
for (a in aritiesExtended) offExt += benchCallsOnce(a, iters)
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = true
|
||||
val onExt = mutableListOf<Long>()
|
||||
for (a in aritiesExtended) onExt += benchCallsOnce(a, iters)
|
||||
appendLine(f, "[DEBUG_LOG] ARG_SMALL_ARITY_12 A/B (iters=$iters):")
|
||||
aritiesExtended.forEachIndexed { idx, a ->
|
||||
appendLine(f, "[DEBUG_LOG] arity=$a OFF=${offExt[idx]} ns, ON=${onExt[idx]} ns, delta=${offExt[idx] - onExt[idx]} ns")
|
||||
}
|
||||
} finally {
|
||||
PerfFlags.ARG_SMALL_ARITY_12 = savedSmall
|
||||
}
|
||||
|
||||
// Optional call short-circuit sanity timing (does not A/B a flag currently; implementation short-circuits before args)
|
||||
val tOpt = benchOptionalCallShortCircuit(100_000)
|
||||
appendLine(f, "[DEBUG_LOG] Optional-call short-circuit sanity: ${tOpt} ns for 100k iterations (side-effect arg not evaluated).")
|
||||
|
||||
// A/B for SCOPE_POOL
|
||||
PerfFlags.SCOPE_POOL = false
|
||||
val tPoolOff = benchCallsOnce(5, iters)
|
||||
PerfFlags.SCOPE_POOL = true
|
||||
val tPoolOn = benchCallsOnce(5, iters)
|
||||
appendLine(f, "[DEBUG_LOG] SCOPE_POOL A/B (arity=5, iters=$iters): OFF=${tPoolOff} ns, ON=${tPoolOn} ns, delta=${tPoolOff - tPoolOn} ns")
|
||||
|
||||
} finally {
|
||||
PerfFlags.ARG_BUILDER = savedArgBuilder
|
||||
PerfFlags.SCOPE_POOL = savedScopePool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Minimal runBlocking for common jvmTest without depending on kotlinx.coroutines test artifacts here
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
class IndexPicABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/index_pic_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Index PIC A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildStringIndexScript(len: Int, iters: Int): Script {
|
||||
// Build a long string and index it by cycling positions
|
||||
val content = (0 until len).joinToString("") { i ->
|
||||
val ch = 'a' + (i % 26)
|
||||
ch.toString()
|
||||
}
|
||||
val src = """
|
||||
val s = "$content"
|
||||
var acc = 0
|
||||
for(i in 0..${iters - 1}) {
|
||||
val j = i % ${len}
|
||||
// Compare to a 1-char string to avoid needing Char.toInt(); still exercises indexing path
|
||||
if (s[j] == "a") { acc += 1 } else { acc += 0 }
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-string>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildMapIndexScript(keys: Int, iters: Int): Script {
|
||||
// Build a map of ("kX" -> X) and repeatedly access by key cycling
|
||||
val entries = (0 until keys).joinToString(", ") { i -> "\"k$i\" => $i" }
|
||||
val src = """
|
||||
// Build via Map(entry1, entry2, ...), not a list literal
|
||||
val m = Map($entries)
|
||||
var acc = 0
|
||||
for(i in 0..${iters - 1}) {
|
||||
val k = "k" + (i % ${keys})
|
||||
acc += (m[k] ?: 0)
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-map>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_index_pic_and_size() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedIndexPic = PerfFlags.INDEX_PIC
|
||||
val savedIndexSize4 = PerfFlags.INDEX_PIC_SIZE_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
|
||||
try {
|
||||
val iters = 300_000
|
||||
val sLen = 512
|
||||
val mapKeys = 256
|
||||
val sScript = buildStringIndexScript(sLen, iters)
|
||||
val mScript = buildMapIndexScript(mapKeys, iters)
|
||||
|
||||
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B on $which (iters=$iters)") }
|
||||
|
||||
// Baseline OFF
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = false
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
header("String[Int], INDEX_PIC=OFF")
|
||||
val tSOff = runOnce(sScript)
|
||||
header("Map[String], INDEX_PIC=OFF")
|
||||
val tMOff = runOnce(mScript)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 2
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
val tSOn2 = runOnce(sScript)
|
||||
val tMOn2 = runOnce(mScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=2 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 4
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = true
|
||||
val tSOn4 = runOnce(sScript)
|
||||
val tMOn4 = runOnce(mScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=4 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] String[Int] OFF=${tSOff} ns, ON(2)=${tSOn2} ns, ON(4)=${tSOn4} ns")
|
||||
appendLine(f, "[DEBUG_LOG] Map[String] OFF=${tMOff} ns, ON(2)=${tMOn2} ns, ON(4)=${tMOn4} ns")
|
||||
} finally {
|
||||
PerfFlags.INDEX_PIC = savedIndexPic
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = savedIndexSize4
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* A/B micro-benchmark for index WRITE paths (Map[String] put, List[Int] set).
|
||||
* Measures OFF vs ON for INDEX_PIC and then size 2 vs 4 (INDEX_PIC_SIZE_4).
|
||||
* Produces [DEBUG_LOG] output in lynglib/build/index_write_ab_results.txt
|
||||
*/
|
||||
class IndexWritePathABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/index_write_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Index WRITE PIC A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildMapWriteScript(keys: Int, iters: Int): Script {
|
||||
// Construct map with keys k0..k{keys-1} and then perform writes in a tight loop
|
||||
val initEntries = (0 until keys).joinToString(", ") { i -> "\"k$i\" => $i" }
|
||||
val src = """
|
||||
var acc = 0
|
||||
val m = Map($initEntries)
|
||||
for(i in 0..${iters - 1}) {
|
||||
val k = "k" + (i % $keys)
|
||||
m[k] = i
|
||||
acc += (m[k] ?: 0)
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-map-write>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildListWriteScript(len: Int, iters: Int): Script {
|
||||
val initList = (0 until len).joinToString(", ") { i -> i.toString() }
|
||||
val src = """
|
||||
var acc = 0
|
||||
val a = [$initList]
|
||||
for(i in 0..${iters - 1}) {
|
||||
val j = i % $len
|
||||
a[j] = i
|
||||
acc += a[j]
|
||||
}
|
||||
acc
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<idx-list-write>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_index_write_paths() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedIndexPic = PerfFlags.INDEX_PIC
|
||||
val savedIndexSize4 = PerfFlags.INDEX_PIC_SIZE_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
|
||||
try {
|
||||
val iters = 250_000
|
||||
val mapKeys = 256
|
||||
val listLen = 1024
|
||||
val mScript = buildMapWriteScript(mapKeys, iters)
|
||||
val lScript = buildListWriteScript(listLen, iters)
|
||||
|
||||
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B on $which (iters=$iters)") }
|
||||
|
||||
// Baseline OFF
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = false
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
header("Map[String] write, INDEX_PIC=OFF")
|
||||
val tMOff = runOnce(mScript)
|
||||
header("List[Int] write, INDEX_PIC=OFF")
|
||||
val tLOff = runOnce(lScript)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 2
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = false
|
||||
val tMOn2 = runOnce(mScript)
|
||||
val tLOn2 = runOnce(lScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=2 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// PIC ON, size 4
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.INDEX_PIC = true
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = true
|
||||
val tMOn4 = runOnce(mScript)
|
||||
val tLOn4 = runOnce(lScript)
|
||||
appendLine(f, "[DEBUG_LOG] ON size=4 counters: indexHit=${PerfStats.indexPicHit} indexMiss=${PerfStats.indexPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] Map[String] WRITE OFF=$tMOff ns, ON(2)=$tMOn2 ns, ON(4)=$tMOn4 ns")
|
||||
appendLine(f, "[DEBUG_LOG] List[Int] WRITE OFF=$tLOff ns, ON(2)=$tLOn2 ns, ON(4)=$tLOn4 ns")
|
||||
} finally {
|
||||
PerfFlags.INDEX_PIC = savedIndexPic
|
||||
PerfFlags.INDEX_PIC_SIZE_4 = savedIndexSize4
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class PerfProfilesTest {
|
||||
|
||||
@Test
|
||||
fun apply_and_restore_presets() {
|
||||
val before = PerfProfiles.snapshot()
|
||||
|
||||
try {
|
||||
// BENCH preset expectations
|
||||
val snapAfterBench = PerfProfiles.apply(PerfProfiles.Preset.BENCH)
|
||||
// Expect some key flags ON for benches
|
||||
assertTrue(PerfFlags.ARG_BUILDER)
|
||||
assertTrue(PerfFlags.SCOPE_POOL)
|
||||
assertTrue(PerfFlags.FIELD_PIC)
|
||||
assertTrue(PerfFlags.METHOD_PIC)
|
||||
assertTrue(PerfFlags.INDEX_PIC)
|
||||
assertTrue(PerfFlags.INDEX_PIC_SIZE_4)
|
||||
assertTrue(PerfFlags.PRIMITIVE_FASTOPS)
|
||||
assertTrue(PerfFlags.RVAL_FASTPATH)
|
||||
// Restore via snapshot returned by apply
|
||||
PerfProfiles.restore(snapAfterBench)
|
||||
|
||||
// BOOKS preset expectations
|
||||
val snapAfterBooks = PerfProfiles.apply(PerfProfiles.Preset.BOOKS)
|
||||
// Expect simpler paths enabled/disabled accordingly
|
||||
assertEquals(false, PerfFlags.ARG_BUILDER)
|
||||
assertEquals(false, PerfFlags.SCOPE_POOL)
|
||||
assertEquals(false, PerfFlags.FIELD_PIC)
|
||||
assertEquals(false, PerfFlags.METHOD_PIC)
|
||||
assertEquals(false, PerfFlags.INDEX_PIC)
|
||||
assertEquals(false, PerfFlags.INDEX_PIC_SIZE_4)
|
||||
assertEquals(false, PerfFlags.PRIMITIVE_FASTOPS)
|
||||
assertEquals(false, PerfFlags.RVAL_FASTPATH)
|
||||
// Restore via snapshot returned by apply
|
||||
PerfProfiles.restore(snapAfterBooks)
|
||||
|
||||
// BASELINE preset should match PerfDefaults
|
||||
val snapAfterBaseline = PerfProfiles.apply(PerfProfiles.Preset.BASELINE)
|
||||
assertEquals(PerfDefaults.ARG_BUILDER, PerfFlags.ARG_BUILDER)
|
||||
assertEquals(PerfDefaults.SCOPE_POOL, PerfFlags.SCOPE_POOL)
|
||||
assertEquals(PerfDefaults.FIELD_PIC, PerfFlags.FIELD_PIC)
|
||||
assertEquals(PerfDefaults.METHOD_PIC, PerfFlags.METHOD_PIC)
|
||||
assertEquals(PerfDefaults.INDEX_PIC_SIZE_4, PerfFlags.INDEX_PIC_SIZE_4)
|
||||
assertEquals(PerfDefaults.PRIMITIVE_FASTOPS, PerfFlags.PRIMITIVE_FASTOPS)
|
||||
assertEquals(PerfDefaults.RVAL_FASTPATH, PerfFlags.RVAL_FASTPATH)
|
||||
// Restore baseline snapshot
|
||||
PerfProfiles.restore(snapAfterBaseline)
|
||||
|
||||
} finally {
|
||||
// Finally, restore very original snapshot
|
||||
PerfProfiles.restore(before)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
class PicAdaptiveABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/pic_adaptive_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] PIC Adaptive 2→4 A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildScriptForMethodShapes(shapes: Int, iters: Int): Script {
|
||||
// Define N classes C0..C{shapes-1} each with method f() { 1 }
|
||||
val classes = (0 until shapes).joinToString("\n") { i ->
|
||||
"class C$i { fun f() { $i } var x = 0 }"
|
||||
}
|
||||
val inits = (0 until shapes).joinToString(", ") { i -> "C$i()" }
|
||||
val calls = buildString {
|
||||
append("var s = 0\n")
|
||||
append("val a = [${inits}]\n")
|
||||
append("for(i in 0..${iters - 1}) {\n")
|
||||
append(" val o = a[i % ${shapes}]\n")
|
||||
append(" s += o.f()\n")
|
||||
append("}\n")
|
||||
append("s\n")
|
||||
}
|
||||
val src = classes + "\n" + calls
|
||||
return Compiler.compile(Source("<pic-method-shapes>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildScriptForFieldShapes(shapes: Int, iters: Int): Script {
|
||||
// Each class has a mutable field x initialized to 0; read and write it
|
||||
val classes = (0 until shapes).joinToString("\n") { i ->
|
||||
"class F$i { var x = 0 }"
|
||||
}
|
||||
val inits = (0 until shapes).joinToString(", ") { i -> "F$i()" }
|
||||
val body = buildString {
|
||||
append("var s = 0\n")
|
||||
append("val a = [${inits}]\n")
|
||||
append("for(i in 0..${iters - 1}) {\n")
|
||||
append(" val o = a[i % ${shapes}]\n")
|
||||
append(" s += o.x\n")
|
||||
append(" o.x = o.x + 1\n")
|
||||
append("}\n")
|
||||
append("s\n")
|
||||
}
|
||||
val src = classes + "\n" + body
|
||||
return Compiler.compile(Source("<pic-field-shapes>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_adaptive_pic() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedAdaptive = PerfFlags.PIC_ADAPTIVE_2_TO_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
val savedFieldPic = PerfFlags.FIELD_PIC
|
||||
val savedMethodPic = PerfFlags.METHOD_PIC
|
||||
val savedFieldPicSize4 = PerfFlags.FIELD_PIC_SIZE_4
|
||||
val savedMethodPicSize4 = PerfFlags.METHOD_PIC_SIZE_4
|
||||
|
||||
try {
|
||||
// Ensure baseline PICs are enabled and fixed-size flags OFF to isolate adaptivity
|
||||
PerfFlags.FIELD_PIC = true
|
||||
PerfFlags.METHOD_PIC = true
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = false
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = false
|
||||
|
||||
// Prepare workloads with 3 and 4 receiver shapes
|
||||
val iters = 200_000
|
||||
val meth3 = buildScriptForMethodShapes(3, iters)
|
||||
val meth4 = buildScriptForMethodShapes(4, iters)
|
||||
val fld3 = buildScriptForFieldShapes(3, iters)
|
||||
val fld4 = buildScriptForFieldShapes(4, iters)
|
||||
|
||||
fun header(which: String) {
|
||||
appendLine(f, "[DEBUG_LOG] A/B Adaptive PIC on $which (iters=$iters)")
|
||||
}
|
||||
|
||||
// OFF pass
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
|
||||
header("methods-3")
|
||||
val tM3Off = runOnce(meth3)
|
||||
header("methods-4")
|
||||
val tM4Off = runOnce(meth4)
|
||||
header("fields-3")
|
||||
val tF3Off = runOnce(fld3)
|
||||
header("fields-4")
|
||||
val tF4Off = runOnce(fld4)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss} fieldHit=${PerfStats.fieldPicHit} fieldMiss=${PerfStats.fieldPicMiss}")
|
||||
|
||||
// ON pass
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = true
|
||||
val tM3On = runOnce(meth3)
|
||||
val tM4On = runOnce(meth4)
|
||||
val tF3On = runOnce(fld3)
|
||||
val tF4On = runOnce(fld4)
|
||||
appendLine(f, "[DEBUG_LOG] ON counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss} fieldHit=${PerfStats.fieldPicHit} fieldMiss=${PerfStats.fieldPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] methods-3 OFF=${tM3Off} ns, ON=${tM3On} ns, delta=${tM3Off - tM3On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] methods-4 OFF=${tM4Off} ns, ON=${tM4On} ns, delta=${tM4Off - tM4On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] fields-3 OFF=${tF3Off} ns, ON=${tF3On} ns, delta=${tF3Off - tF3On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] fields-4 OFF=${tF4Off} ns, ON=${tF4On} ns, delta=${tF4Off - tF4On} ns")
|
||||
} finally {
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = savedAdaptive
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
PerfFlags.FIELD_PIC = savedFieldPic
|
||||
PerfFlags.METHOD_PIC = savedMethodPic
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = savedFieldPicSize4
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = savedMethodPicSize4
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* A/B micro-benchmark to compare methods-only adaptive PIC OFF vs ON.
|
||||
* Ensures fixed PIC sizes (2-entry) and only toggles PIC_ADAPTIVE_METHODS_ONLY.
|
||||
* Writes a summary to lynglib/build/pic_methods_only_adaptive_ab_results.txt
|
||||
*/
|
||||
class PicMethodsOnlyAdaptiveABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/pic_methods_only_adaptive_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] PIC Adaptive (methods-only) 2→4 A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildScriptForMethodShapes(shapes: Int, iters: Int): Script {
|
||||
// Define N classes C0..C{shapes-1} each with method f() { i }
|
||||
val classes = (0 until shapes).joinToString("\n") { i ->
|
||||
"class MC$i { fun f() { $i } }"
|
||||
}
|
||||
val inits = (0 until shapes).joinToString(", ") { i -> "MC$i()" }
|
||||
val body = buildString {
|
||||
append("var s = 0\n")
|
||||
append("val a = [${inits}]\n")
|
||||
append("for(i in 0..${iters - 1}) {\n")
|
||||
append(" val o = a[i % ${shapes}]\n")
|
||||
append(" s += o.f()\n")
|
||||
append("}\n")
|
||||
append("s\n")
|
||||
}
|
||||
val src = classes + "\n" + body
|
||||
return Compiler.compile(Source("<pic-meth-only-shapes>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_methods_only_adaptive_pic() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
// Save flags
|
||||
val savedAdaptive2To4 = PerfFlags.PIC_ADAPTIVE_2_TO_4
|
||||
val savedAdaptiveMethodsOnly = PerfFlags.PIC_ADAPTIVE_METHODS_ONLY
|
||||
val savedFieldPic = PerfFlags.FIELD_PIC
|
||||
val savedMethodPic = PerfFlags.METHOD_PIC
|
||||
val savedFieldSize4 = PerfFlags.FIELD_PIC_SIZE_4
|
||||
val savedMethodSize4 = PerfFlags.METHOD_PIC_SIZE_4
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
|
||||
try {
|
||||
// Fixed-size 2-entry PICs, enable PICs, disable global adaptivity
|
||||
PerfFlags.FIELD_PIC = true
|
||||
PerfFlags.METHOD_PIC = true
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = false
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = false
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = false
|
||||
|
||||
val iters = 200_000
|
||||
val meth3 = buildScriptForMethodShapes(3, iters)
|
||||
val meth4 = buildScriptForMethodShapes(4, iters)
|
||||
|
||||
fun header(which: String) { appendLine(f, "[DEBUG_LOG] A/B Methods-only adaptive on $which (iters=$iters)") }
|
||||
|
||||
// OFF pass
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = false
|
||||
header("methods-3")
|
||||
val tM3Off = runOnce(meth3)
|
||||
header("methods-4")
|
||||
val tM4Off = runOnce(meth4)
|
||||
appendLine(f, "[DEBUG_LOG] OFF counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss}")
|
||||
|
||||
// ON pass
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = true
|
||||
val tM3On = runOnce(meth3)
|
||||
val tM4On = runOnce(meth4)
|
||||
appendLine(f, "[DEBUG_LOG] ON counters: methodHit=${PerfStats.methodPicHit} methodMiss=${PerfStats.methodPicMiss}")
|
||||
|
||||
// Report
|
||||
appendLine(f, "[DEBUG_LOG] methods-3 OFF=${tM3Off} ns, ON=${tM3On} ns, delta=${tM3Off - tM3On} ns")
|
||||
appendLine(f, "[DEBUG_LOG] methods-4 OFF=${tM4Off} ns, ON=${tM4On} ns, delta=${tM4Off - tM4On} ns")
|
||||
} finally {
|
||||
// Restore
|
||||
PerfFlags.PIC_ADAPTIVE_2_TO_4 = savedAdaptive2To4
|
||||
PerfFlags.PIC_ADAPTIVE_METHODS_ONLY = savedAdaptiveMethodsOnly
|
||||
PerfFlags.FIELD_PIC = savedFieldPic
|
||||
PerfFlags.METHOD_PIC = savedMethodPic
|
||||
PerfFlags.FIELD_PIC_SIZE_4 = savedFieldSize4
|
||||
PerfFlags.METHOD_PIC_SIZE_4 = savedMethodSize4
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
* A/B micro-benchmark to compare PerfFlags.PRIMITIVE_FASTOPS OFF vs ON.
|
||||
* JVM-only quick check using simple arithmetic/logic loops.
|
||||
*/
|
||||
package net.sergeych.lyng
|
||||
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
class PrimitiveFastOpsABTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/primitive_ab_results.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Primitive FastOps A/B results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) {
|
||||
f.appendText(s + "\n")
|
||||
}
|
||||
|
||||
private fun benchIntArithmeticIters(iters: Int): Long {
|
||||
var acc = 0L
|
||||
val t = measureNanoTime {
|
||||
var a = 1L
|
||||
var b = 2L
|
||||
var c = 3L
|
||||
repeat(iters) {
|
||||
// mimic mix of +, -, *, /, %, shifts and comparisons
|
||||
a = (a + b) xor c
|
||||
b = (b * 3L + a) and 0x7FFF_FFFFL
|
||||
if ((b and 1L) == 0L) c = c + 1L else c = c - 1L
|
||||
acc = acc + (a and b) + (c or a)
|
||||
}
|
||||
}
|
||||
// use acc to prevent DCE
|
||||
if (acc == 42L) println("[DEBUG_LOG] impossible")
|
||||
return t
|
||||
}
|
||||
|
||||
private fun benchBoolLogicIters(iters: Int): Long {
|
||||
var acc = 0
|
||||
val t = measureNanoTime {
|
||||
var a = true
|
||||
var b = false
|
||||
repeat(iters) {
|
||||
a = a || b
|
||||
b = !b && a
|
||||
if (a == b) acc++ else acc--
|
||||
}
|
||||
}
|
||||
if (acc == Int.MIN_VALUE) println("[DEBUG_LOG] impossible2")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ab_compare_primitive_fastops() {
|
||||
// Save current settings
|
||||
val savedFast = PerfFlags.PRIMITIVE_FASTOPS
|
||||
val savedCounters = PerfFlags.PIC_DEBUG_COUNTERS
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
try {
|
||||
val iters = 500_000
|
||||
|
||||
// OFF pass
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = true
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PRIMITIVE_FASTOPS = false
|
||||
val tArithOff = benchIntArithmeticIters(iters)
|
||||
val tLogicOff = benchBoolLogicIters(iters)
|
||||
|
||||
// ON pass
|
||||
PerfStats.resetAll()
|
||||
PerfFlags.PRIMITIVE_FASTOPS = true
|
||||
val tArithOn = benchIntArithmeticIters(iters)
|
||||
val tLogicOn = benchBoolLogicIters(iters)
|
||||
|
||||
println("[DEBUG_LOG] A/B PrimitiveFastOps (iters=$iters):")
|
||||
println("[DEBUG_LOG] Arithmetic OFF: ${'$'}tArithOff ns, ON: ${'$'}tArithOn ns, delta: ${'$'}{tArithOff - tArithOn} ns")
|
||||
println("[DEBUG_LOG] Bool logic OFF: ${'$'}tLogicOff ns, ON: ${'$'}tLogicOn ns, delta: ${'$'}{tLogicOff - tLogicOn} ns")
|
||||
|
||||
appendLine(f, "[DEBUG_LOG] A/B PrimitiveFastOps (iters=$iters):")
|
||||
appendLine(f, "[DEBUG_LOG] Arithmetic OFF: ${'$'}tArithOff ns, ON: ${'$'}tArithOn ns, delta: ${'$'}{tArithOff - tArithOn} ns")
|
||||
appendLine(f, "[DEBUG_LOG] Bool logic OFF: ${'$'}tLogicOff ns, ON: ${'$'}tLogicOn ns, delta: ${'$'}{tLogicOff - tLogicOn} ns")
|
||||
} finally {
|
||||
// restore
|
||||
PerfFlags.PRIMITIVE_FASTOPS = savedFast
|
||||
PerfFlags.PIC_DEBUG_COUNTERS = savedCounters
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package net.sergeych.lyng
|
||||
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
import net.sergeych.lyng.obj.ObjInt
|
||||
import java.io.File
|
||||
import kotlin.system.measureNanoTime
|
||||
import kotlin.test.Test
|
||||
|
||||
/**
|
||||
* Baseline range iteration benchmark. It measures for-loops over integer ranges under
|
||||
* current implementation and records timings. When RANGE_FAST_ITER is implemented,
|
||||
* this test will also serve for OFF vs ON A/B.
|
||||
*/
|
||||
class RangeIterationBenchmarkTest {
|
||||
|
||||
private fun outFile(): File = File("lynglib/build/range_iter_bench.txt")
|
||||
|
||||
private fun writeHeader(f: File) {
|
||||
if (!f.parentFile.exists()) f.parentFile.mkdirs()
|
||||
f.writeText("[DEBUG_LOG] Range iteration benchmark results\n")
|
||||
}
|
||||
|
||||
private fun appendLine(f: File, s: String) { f.appendText(s + "\n") }
|
||||
|
||||
private suspend fun buildSumScriptInclusive(n: Int, iters: Int): Script {
|
||||
// Sum 0..n repeatedly to stress iteration
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in 0..$n) { s += i }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-inc>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptExclusive(n: Int, iters: Int): Script {
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in 0..<$n) { s += i }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-exc>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptReversed(n: Int, iters: Int): Script {
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
// reversed-like loop using countdown range (n..0)
|
||||
for (i in $n..0) { s += i }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-rev>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptNegative(n: Int, iters: Int): Script {
|
||||
// Sum -n..n repeatedly
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in -$n..$n) { s += (i < 0 ? -i : i) }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-neg>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun buildSumScriptEmpty(iters: Int): Script {
|
||||
// Empty range 1..0 should not iterate
|
||||
val src = """
|
||||
var total = 0
|
||||
for (k in 0..${iters - 1}) {
|
||||
var s = 0
|
||||
for (i in 1..0) { s += 1 }
|
||||
total += s
|
||||
}
|
||||
total
|
||||
""".trimIndent()
|
||||
return Compiler.compile(Source("<range-empty>", src), Script.defaultImportManager)
|
||||
}
|
||||
|
||||
private suspend fun runOnce(script: Script): Long {
|
||||
val scope = Script.newScope()
|
||||
var result: Obj? = null
|
||||
val t = measureNanoTime { result = script.execute(scope) }
|
||||
if (result !is ObjInt) println("[DEBUG_LOG] result=${result?.javaClass?.simpleName}")
|
||||
return t
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bench_range_iteration_baseline() = runTestBlocking {
|
||||
val f = outFile()
|
||||
writeHeader(f)
|
||||
|
||||
val savedFlag = PerfFlags.RANGE_FAST_ITER
|
||||
try {
|
||||
val n = 1000
|
||||
val iters = 500
|
||||
|
||||
// Baseline with current flag (OFF by default)
|
||||
PerfFlags.RANGE_FAST_ITER = false
|
||||
val sIncOff = buildSumScriptInclusive(n, iters)
|
||||
val tIncOff = runOnce(sIncOff)
|
||||
val sExcOff = buildSumScriptExclusive(n, iters)
|
||||
val tExcOff = runOnce(sExcOff)
|
||||
appendLine(f, "[DEBUG_LOG] OFF inclusive=${tIncOff} ns, exclusive=${tExcOff} ns")
|
||||
|
||||
// Also record ON times
|
||||
PerfFlags.RANGE_FAST_ITER = true
|
||||
val sIncOn = buildSumScriptInclusive(n, iters)
|
||||
val tIncOn = runOnce(sIncOn)
|
||||
val sExcOn = buildSumScriptExclusive(n, iters)
|
||||
val tExcOn = runOnce(sExcOn)
|
||||
appendLine(f, "[DEBUG_LOG] ON inclusive=${tIncOn} ns, exclusive=${tExcOn} ns")
|
||||
|
||||
// Additional scenarios: reversed, negative, empty
|
||||
PerfFlags.RANGE_FAST_ITER = false
|
||||
val sRevOff = buildSumScriptReversed(n, iters)
|
||||
val tRevOff = runOnce(sRevOff)
|
||||
val sNegOff = buildSumScriptNegative(n, iters)
|
||||
val tNegOff = runOnce(sNegOff)
|
||||
val sEmptyOff = buildSumScriptEmpty(iters)
|
||||
val tEmptyOff = runOnce(sEmptyOff)
|
||||
appendLine(f, "[DEBUG_LOG] OFF reversed=${tRevOff} ns, negative=${tNegOff} ns, empty=${tEmptyOff} ns")
|
||||
|
||||
PerfFlags.RANGE_FAST_ITER = true
|
||||
val sRevOn = buildSumScriptReversed(n, iters)
|
||||
val tRevOn = runOnce(sRevOn)
|
||||
val sNegOn = buildSumScriptNegative(n, iters)
|
||||
val tNegOn = runOnce(sNegOn)
|
||||
val sEmptyOn = buildSumScriptEmpty(iters)
|
||||
val tEmptyOn = runOnce(sEmptyOn)
|
||||
appendLine(f, "[DEBUG_LOG] ON reversed=${tRevOn} ns, negative=${tNegOn} ns, empty=${tEmptyOn} ns")
|
||||
} finally {
|
||||
PerfFlags.RANGE_FAST_ITER = savedFlag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun runTestBlocking(block: suspend () -> Unit) {
|
||||
kotlinx.coroutines.runBlocking { block() }
|
||||
}
|
||||
Reference in new issue
Block a user