/* * 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("", 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("", 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() for (a in aritiesBase) offTimes += benchCallsOnce(a, iters) PerfFlags.ARG_BUILDER = true val onTimes = mutableListOf() 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() for (a in aritiesExtended) offExt += benchCallsOnce(a, iters) PerfFlags.ARG_SMALL_ARITY_12 = true val onExt = mutableListOf() 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() } }