Refactor scope pooling logic for robustness and efficiency, optimize argument assignment, and enhance benchmarks with pool-specific scenarios.

This commit is contained in:
Sergey Chernov 2026-01-11 02:50:08 +01:00
parent 6b957ae6a3
commit 8f04b25fcb
5 changed files with 133 additions and 53 deletions

View File

@ -51,6 +51,32 @@ data class ArgsDeclaration(val params: List<Item>, val endTokenType: Token.Type)
defaultVisibility: Visibility = Visibility.Public,
declaringClass: net.sergeych.lyng.obj.ObjClass? = scope.currentClassCtx
) {
// Fast path for simple positional-only calls with no ellipsis and no defaults
if (arguments.named.isEmpty() && !arguments.tailBlockMode) {
var hasComplex = false
for (p in params) {
if (p.isEllipsis || p.defaultValue != null) {
hasComplex = true
break
}
}
if (!hasComplex) {
if (arguments.list.size != params.size)
scope.raiseIllegalArgument("expected ${params.size} arguments, got ${arguments.list.size}")
for (i in params.indices) {
val a = params[i]
val value = arguments.list[i]
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
value.byValueCopy(),
a.visibility ?: defaultVisibility,
recordType = ObjRecord.Type.Argument,
declaringClass = declaringClass)
}
return
}
}
fun assign(a: Item, value: Obj) {
scope.addItem(a.name, (a.accessType ?: defaultAccessType).isMutable,
value.byValueCopy(),

View File

@ -70,9 +70,8 @@ open class Scope(
internal fun findExtension(receiverClass: ObjClass, name: String): ObjRecord? {
var s: Scope? = this
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) break
var hops = 0
while (s != null && hops++ < 1024) {
// Proximity rule: check all extensions in the current scope before going to parent.
// Priority within scope: more specific class in MRO wins.
for (cls in receiverClass.mro) {
@ -108,7 +107,7 @@ open class Scope(
*/
internal fun tryGetLocalRecord(s: Scope, name: String, caller: net.sergeych.lyng.obj.ObjClass?): ObjRecord? {
caller?.let { ctx ->
s.objects["${ctx.className}::$name"]?.let { rec ->
s.objects[ctx.mangledName(name)]?.let { rec ->
if (rec.visibility == Visibility.Private) return rec
}
}
@ -116,7 +115,7 @@ open class Scope(
if (rec.declaringClass == null || canAccessMember(rec.visibility, rec.declaringClass, caller)) return rec
}
caller?.let { ctx ->
s.localBindings["${ctx.className}::$name"]?.let { rec ->
s.localBindings[ctx.mangledName(name)]?.let { rec ->
if (rec.visibility == Visibility.Private) return rec
}
}
@ -132,11 +131,10 @@ open class Scope(
internal fun chainLookupIgnoreClosure(name: String, followClosure: Boolean = false, caller: net.sergeych.lyng.obj.ObjClass? = null): ObjRecord? {
var s: Scope? = this
// use frameId to detect unexpected structural cycles in the parent chain
val visited = HashSet<Long>(4)
// use hop counter to detect unexpected structural cycles in the parent chain
var hops = 0
val effectiveCaller = caller ?: currentClassCtx
while (s != null) {
if (!visited.add(s.frameId)) return null
while (s != null && hops++ < 1024) {
tryGetLocalRecord(s, name, effectiveCaller)?.let { return it }
s = if (followClosure && s is ClosureScope) s.closureScope else s.parent
}
@ -155,9 +153,8 @@ open class Scope(
tryGetLocalRecord(this, name, currentClassCtx)?.let { return it }
// 2) walk parents for plain locals/bindings only
var s = parent
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) return null
var hops = 0
while (s != null && hops++ < 1024) {
tryGetLocalRecord(s, name, currentClassCtx)?.let { return it }
s = s.parent
}
@ -182,9 +179,8 @@ open class Scope(
*/
internal fun chainLookupWithMembers(name: String, caller: net.sergeych.lyng.obj.ObjClass? = currentClassCtx, followClosure: Boolean = false): ObjRecord? {
var s: Scope? = this
val visited = HashSet<Long>(4)
while (s != null) {
if (!visited.add(s.frameId)) return null
var hops = 0
while (s != null && hops++ < 1024) {
tryGetLocalRecord(s, name, caller)?.let { return it }
for (cls in s.thisObj.objClass.mro) {
s.extensions[cls]?.get(name)?.let { return it }
@ -396,6 +392,20 @@ open class Scope(
nameToSlot[name]?.let { slots[it] = record }
}
/**
* Clear all references and maps to prevent memory leaks when pooled.
*/
fun scrub() {
this.parent = null
this.skipScopeCreation = false
this.currentClassCtx = null
objects.clear()
slots.clear()
nameToSlot.clear()
localBindings.clear()
extensions.clear()
}
/**
* Reset this scope instance so it can be safely reused as a fresh child frame.
* Clears locals and slots, assigns new frameId, and sets parent/args/pos/thisObj.
@ -414,7 +424,6 @@ open class Scope(
nameToSlot.clear()
localBindings.clear()
extensions.clear()
this.currentClassCtx = parent?.currentClassCtx
// Now safe to validate and re-parent
ensureNoCycle(parent)
this.parent = parent
@ -534,11 +543,15 @@ open class Scope(
}
}
// Map to a slot for fast local access (ensure consistency)
val idx = getSlotIndexOf(name)
if (idx == null) {
if (nameToSlot.isEmpty()) {
allocateSlotFor(name, rec)
} else {
slots[idx] = rec
val idx = nameToSlot[name]
if (idx == null) {
allocateSlotFor(name, rec)
} else {
slots[idx] = rec
}
}
return rec
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 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.
@ -18,25 +18,25 @@
package net.sergeych.lyng
actual object PerfDefaults {
actual val LOCAL_SLOT_PIC: Boolean = false
actual val EMIT_FAST_LOCAL_REFS: Boolean = false
actual val LOCAL_SLOT_PIC: Boolean = true
actual val EMIT_FAST_LOCAL_REFS: Boolean = true
actual val ARG_BUILDER: Boolean = false
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = false
actual val ARG_BUILDER: Boolean = true
actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true
actual val SCOPE_POOL: Boolean = false
actual val FIELD_PIC: Boolean = false
actual val METHOD_PIC: Boolean = false
actual val FIELD_PIC_SIZE_4: Boolean = false
actual val METHOD_PIC_SIZE_4: Boolean = false
actual val PIC_ADAPTIVE_2_TO_4: Boolean = false
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = false
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = false
actual val FIELD_PIC: Boolean = true
actual val METHOD_PIC: Boolean = true
actual val FIELD_PIC_SIZE_4: Boolean = true
actual val METHOD_PIC_SIZE_4: Boolean = true
actual val PIC_ADAPTIVE_2_TO_4: Boolean = true
actual val PIC_ADAPTIVE_METHODS_ONLY: Boolean = true
actual val PIC_ADAPTIVE_HEURISTIC: Boolean = true
actual val PIC_DEBUG_COUNTERS: Boolean = false
actual val PRIMITIVE_FASTOPS: Boolean = false
actual val RVAL_FASTPATH: Boolean = false
actual val PRIMITIVE_FASTOPS: Boolean = true
actual val RVAL_FASTPATH: Boolean = true
// Regex caching (JVM-first): enabled by default on JVM
actual val REGEX_CACHE: Boolean = true

View File

@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 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.
@ -18,7 +18,6 @@
package net.sergeych.lyng
import net.sergeych.lyng.obj.Obj
import net.sergeych.lyng.obj.ObjVoid
/**
* JVM actual: per-thread scope frame pool backed by ThreadLocal.
@ -32,26 +31,31 @@ actual object ScopePool {
actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope {
val pool = threadLocalPool.get()
val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj)
return try {
// Always reset state on borrow to guarantee fresh-frame semantics
s.resetForReuse(parent, args, pos, thisObj)
s
} catch (e: IllegalStateException) {
// Defensive fallback: if a cycle in scope parent chain is detected during reuse,
// discard pooled instance for this call frame and allocate a fresh scope instead.
if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) {
Scope(parent, args, pos, thisObj)
} else {
throw e
if (pool.isNotEmpty()) {
val s = pool.removeLast()
try {
// Re-initialize pooled instance
s.resetForReuse(parent, args, pos, thisObj)
return s
} catch (e: IllegalStateException) {
// Defensive fallback: if a cycle in scope parent chain is detected during reuse,
// discard pooled instance for this call frame and allocate a fresh scope instead.
if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) {
return Scope(parent, args, pos, thisObj)
} else {
throw e
}
}
}
return Scope(parent, args, pos, thisObj)
}
actual fun release(scope: Scope) {
val pool = threadLocalPool.get()
// Scrub sensitive references to avoid accidental retention
scope.resetForReuse(parent = null, args = Arguments.EMPTY, pos = Pos.builtIn, thisObj = ObjVoid)
if (pool.size < MAX_POOL_SIZE) pool.addLast(scope)
if (pool.size < MAX_POOL_SIZE) {
// Scrub sensitive references to avoid accidental retention before returning to pool
scope.scrub()
pool.addLast(scope)
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2025 Sergey S. Chernov real.sergeych@gmail.com
* Copyright 2026 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.
@ -88,20 +88,57 @@ class PicBenchmarkTest {
// PIC OFF
PerfFlags.METHOD_PIC = false
PerfFlags.SCOPE_POOL = false
val scope1 = Scope()
val t0 = System.nanoTime()
val r1 = (scope1.eval(script) as ObjInt).value
val t1 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Method PIC=OFF: ${(t1 - t0) / 1_000_000.0} ms")
println("[DEBUG_LOG] [BENCH] Method PIC=OFF, POOL=OFF: ${(t1 - t0) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r1)
// PIC ON
PerfFlags.METHOD_PIC = true
PerfFlags.SCOPE_POOL = true
val scope2 = Scope()
val t2 = System.nanoTime()
val r2 = (scope2.eval(script) as ObjInt).value
val t3 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Method PIC=ON: ${(t3 - t2) / 1_000_000.0} ms")
println("[DEBUG_LOG] [BENCH] Method PIC=ON, POOL=ON: ${(t3 - t2) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r2)
}
@Test
fun benchmarkLoopScopePooling() = runBlocking {
val iterations = 500_000
val script = """
var x = 0
var i = 0
while(i < $iterations) {
if(true) {
var y = 1
x = x + y
}
i = i + 1
}
x
""".trimIndent()
// POOL OFF
PerfFlags.SCOPE_POOL = false
val scope1 = Scope()
val t0 = System.nanoTime()
val r1 = (scope1.eval(script) as ObjInt).value
val t1 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Loop Pool=OFF: ${(t1 - t0) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r1)
// POOL ON
PerfFlags.SCOPE_POOL = true
val scope2 = Scope()
val t2 = System.nanoTime()
val r2 = (scope2.eval(script) as ObjInt).value
val t3 = System.nanoTime()
println("[DEBUG_LOG] [BENCH] Loop Pool=ON: ${(t3 - t2) / 1_000_000.0} ms")
assertEquals(iterations.toLong(), r2)
}
}