From f6deabaa38b53335d10a221bf595657ca86f3abf Mon Sep 17 00:00:00 2001 From: sergeych Date: Mon, 12 Jan 2026 06:16:22 +0100 Subject: [PATCH] Enable `SCOPE_POOL` globally across all platforms and refactor pooling logic to enhance robustness, efficiency, and cleanup mechanisms. Update documentation to reflect changes. --- docs/perf_guide.md | 2 +- .../net/sergeych/lyng/PerfDefaults.android.kt | 4 +- .../net/sergeych/lyng/ScopePoolAndroid.kt | 34 ++++++++++------- .../kotlin/net/sergeych/lyng/Compiler.kt | 4 +- .../kotlin/net/sergeych/lyng/ScopePool.kt | 4 +- .../net/sergeych/lyng/obj/ObjException.kt | 4 +- .../net/sergeych/lyng/PerfDefaults.js.kt | 4 +- .../kotlin/net/sergeych/lyng/ScopePoolJs.kt | 34 ++++++++++------- .../net/sergeych/lyng/PerfDefaults.jvm.kt | 6 +-- .../net/sergeych/lyng/PerfDefaults.native.kt | 4 +- .../net/sergeych/lyng/ScopePoolNative.kt | 37 +++++++++++-------- .../net/sergeych/lyng/PerfDefaults.wasmJs.kt | 4 +- .../kotlin/net/sergeych/lyng/ScopePoolWasm.kt | 34 ++++++++++------- 13 files changed, 101 insertions(+), 74 deletions(-) diff --git a/docs/perf_guide.md b/docs/perf_guide.md index fa58286..ff98fee 100644 --- a/docs/perf_guide.md +++ b/docs/perf_guide.md @@ -33,7 +33,7 @@ PerfProfiles.restore(snap) // restore previous flags - `ARG_BUILDER` — Efficient argument building: small‑arity no‑alloc and pooled builder on JVM (ON JVM default). - `ARG_SMALL_ARITY_12` — Extends small‑arity no‑alloc call paths from 0–8 to 0–12 arguments (JVM‑first exploration; OFF by default). Use for codebases with many 9–12 arg calls; A/B before enabling. - `SKIP_ARGS_ON_NULL_RECEIVER` — Early return on optional‑null receivers before building args (semantics‑compatible). A/B only. -- `SCOPE_POOL` — Scope frame pooling for calls (JVM, per‑thread ThreadLocal pool). ON by default on JVM; togglable at runtime. +- `SCOPE_POOL` — Scope frame pooling for calls (per‑thread ThreadLocal pool on JVM/Android/Native; global deque on JS/Wasm). ON by default on all platforms; togglable at runtime. - `FIELD_PIC` — 2‑entry polymorphic inline cache for field reads/writes keyed by `(classId, layoutVersion)` (ON JVM default). - `METHOD_PIC` — 2‑entry PIC for instance method calls keyed by `(classId, layoutVersion)` (ON JVM default). - `FIELD_PIC_SIZE_4` — Increases Field PIC size from 2 to 4 entries (JVM-first tuning; OFF by default). Use for sites with >2 receiver shapes. diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt index 2e2e31b..c5fb7c2 100644 --- a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/PerfDefaults.android.kt @@ -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. @@ -23,7 +23,7 @@ actual object PerfDefaults { actual val ARG_BUILDER: Boolean = true actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true - actual val SCOPE_POOL: Boolean = false + actual val SCOPE_POOL: Boolean = true actual val FIELD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true diff --git a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ScopePoolAndroid.kt b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ScopePoolAndroid.kt index 6f50103..987bbb1 100644 --- a/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ScopePoolAndroid.kt +++ b/lynglib/src/androidMain/kotlin/net/sergeych/lyng/ScopePoolAndroid.kt @@ -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 /** * Android actual: per-thread scope frame pool backed by ThreadLocal. @@ -38,24 +37,31 @@ actual object ScopePool { actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope { val pool = pool() - val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj) - return try { - if (s.parent !== parent || s.args !== args || s.pos !== pos || s.thisObj !== thisObj) { + if (pool.isNotEmpty()) { + val s = pool.removeLast() + try { + // Re-initialize pooled instance s.resetForReuse(parent, args, pos, thisObj) - } else { - s.frameId = nextFrameId() + 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 + } } - s - } catch (e: IllegalStateException) { - if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) { - Scope(parent, args, pos, thisObj) - } else throw e } + return Scope(parent, args, pos, thisObj) } actual fun release(scope: Scope) { val pool = pool() - 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) + } } } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index f191e17..87d642e 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -1734,7 +1734,7 @@ class Compiler( // Rebind error scope to the throw-site position so ScriptError.pos is accurate val throwScope = sc.createChildScope(pos = start) if (errorObject is ObjString) { - errorObject = ObjException(throwScope, errorObject.value) + errorObject = ObjException(throwScope, errorObject.value).apply { getStackTrace() } } if (!errorObject.isInstanceOf(ObjException.Root)) { throwScope.raiseError("this is not an exception object: $errorObject") @@ -1746,7 +1746,7 @@ class Compiler( errorObject.message, errorObject.extraData, errorObject.useStackTrace - ) + ).apply { getStackTrace() } throwScope.raiseError(errorObject) } else { val msg = errorObject.invokeInstanceMethod(sc, "message").toString(sc).value diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt index e853e7f..608e81d 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/ScopePool.kt @@ -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. @@ -21,7 +21,7 @@ import net.sergeych.lyng.obj.Obj /** * Expect/actual portable scope frame pool. Used only when [PerfFlags.SCOPE_POOL] is true. - * JVM actual provides a ThreadLocal-backed pool; other targets may use a simple global deque. + * Provides per-thread pooling on JVM, Android, and Native; global pooling on JS and Wasm. */ expect object ScopePool { fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt index 9143dce..0a08c6f 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/obj/ObjException.kt @@ -129,7 +129,9 @@ open class ObjException( override suspend fun callOn(scope: Scope): Obj { val message = scope.args.getOrNull(0)?.toString(scope) ?: ObjString(name) - return ObjException(this, scope, message) + val ex = ObjException(this, scope, message) + ex.getStackTrace() + return ex } override fun toString(): String = name diff --git a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt index 907fec7..f58f35a 100644 --- a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt +++ b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/PerfDefaults.js.kt @@ -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. @@ -23,7 +23,7 @@ actual object PerfDefaults { actual val ARG_BUILDER: Boolean = true actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true - actual val SCOPE_POOL: Boolean = false + actual val SCOPE_POOL: Boolean = true actual val FIELD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true diff --git a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/ScopePoolJs.kt b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/ScopePoolJs.kt index f30dd6a..ffc323c 100644 --- a/lynglib/src/jsMain/kotlin/net/sergeych/lyng/ScopePoolJs.kt +++ b/lynglib/src/jsMain/kotlin/net/sergeych/lyng/ScopePoolJs.kt @@ -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 /** * JS actual: simple global deque pool (single-threaded runtime). @@ -28,23 +27,30 @@ actual object ScopePool { private val pool = ArrayDeque(MAX_POOL_SIZE) actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope { - val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj) - return try { - if (s.parent !== parent || s.args !== args || s.pos !== pos || s.thisObj !== thisObj) { + if (pool.isNotEmpty()) { + val s = pool.removeLast() + try { + // Re-initialize pooled instance s.resetForReuse(parent, args, pos, thisObj) - } else { - s.frameId = nextFrameId() + 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 + } } - s - } catch (e: IllegalStateException) { - if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) { - Scope(parent, args, pos, thisObj) - } else throw e } + return Scope(parent, args, pos, thisObj) } actual fun release(scope: Scope) { - 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) + } } } diff --git a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt index 713a394..38803ee 100644 --- a/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt +++ b/lynglib/src/jvmMain/kotlin/net/sergeych/lyng/PerfDefaults.jvm.kt @@ -23,7 +23,7 @@ actual object PerfDefaults { actual val ARG_BUILDER: Boolean = true actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true - actual val SCOPE_POOL: Boolean = false + actual val SCOPE_POOL: Boolean = true actual val FIELD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true @@ -42,11 +42,11 @@ actual object PerfDefaults { actual val REGEX_CACHE: Boolean = true // Extended small-arity calls 9..12 (experimental; keep OFF by default) - actual val ARG_SMALL_ARITY_12: Boolean = false + actual val ARG_SMALL_ARITY_12: Boolean = true // Index PIC size (beneficial on JVM in A/B): enable size=4 by default actual val INDEX_PIC_SIZE_4: Boolean = true // Range fast-iteration (experimental; OFF by default) - actual val RANGE_FAST_ITER: Boolean = false + actual val RANGE_FAST_ITER: Boolean = true } \ No newline at end of file diff --git a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt index 85ed428..2f75151 100644 --- a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/PerfDefaults.native.kt @@ -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. @@ -23,7 +23,7 @@ actual object PerfDefaults { actual val ARG_BUILDER: Boolean = true actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true - actual val SCOPE_POOL: Boolean = false + actual val SCOPE_POOL: Boolean = true actual val FIELD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true diff --git a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ScopePoolNative.kt b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ScopePoolNative.kt index 8ae449c..5029773 100644 --- a/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ScopePoolNative.kt +++ b/lynglib/src/nativeMain/kotlin/net/sergeych/lyng/ScopePoolNative.kt @@ -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,33 +18,40 @@ package net.sergeych.lyng import net.sergeych.lyng.obj.Obj -import net.sergeych.lyng.obj.ObjVoid /** - * Native actual: simple global deque pool. Many native targets are single-threaded by default in our setup. + * Native actual: per-thread scope frame pool using @ThreadLocal. */ +@kotlin.native.concurrent.ThreadLocal actual object ScopePool { private const val MAX_POOL_SIZE = 64 private val pool = ArrayDeque(MAX_POOL_SIZE) actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope { - val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj) - return try { - if (s.parent !== parent || s.args !== args || s.pos !== pos || s.thisObj !== thisObj) { + if (pool.isNotEmpty()) { + val s = pool.removeLast() + try { + // Re-initialize pooled instance s.resetForReuse(parent, args, pos, thisObj) - } else { - s.frameId = nextFrameId() + 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 + } } - s - } catch (e: IllegalStateException) { - if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) { - Scope(parent, args, pos, thisObj) - } else throw e } + return Scope(parent, args, pos, thisObj) } actual fun release(scope: Scope) { - 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) + } } } diff --git a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt index ace1e23..e5fb671 100644 --- a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt +++ b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/PerfDefaults.wasmJs.kt @@ -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. @@ -23,7 +23,7 @@ actual object PerfDefaults { actual val ARG_BUILDER: Boolean = true actual val SKIP_ARGS_ON_NULL_RECEIVER: Boolean = true - actual val SCOPE_POOL: Boolean = false + actual val SCOPE_POOL: Boolean = true actual val FIELD_PIC: Boolean = true actual val METHOD_PIC: Boolean = true diff --git a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ScopePoolWasm.kt b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ScopePoolWasm.kt index cd5590a..7dca848 100644 --- a/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ScopePoolWasm.kt +++ b/lynglib/src/wasmJsMain/kotlin/net/sergeych/lyng/ScopePoolWasm.kt @@ -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 /** * Wasm/JS actual: simple global deque pool (single-threaded runtime model). @@ -28,23 +27,30 @@ actual object ScopePool { private val pool = ArrayDeque(MAX_POOL_SIZE) actual fun borrow(parent: Scope, args: Arguments, pos: Pos, thisObj: Obj): Scope { - val s = if (pool.isNotEmpty()) pool.removeLast() else Scope(parent, args, pos, thisObj) - return try { - if (s.parent !== parent || s.args !== args || s.pos !== pos || s.thisObj !== thisObj) { + if (pool.isNotEmpty()) { + val s = pool.removeLast() + try { + // Re-initialize pooled instance s.resetForReuse(parent, args, pos, thisObj) - } else { - s.frameId = nextFrameId() + 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 + } } - s - } catch (e: IllegalStateException) { - if (e.message?.contains("cycle") == true && e.message?.contains("scope parent chain") == true) { - Scope(parent, args, pos, thisObj) - } else throw e } + return Scope(parent, args, pos, thisObj) } actual fun release(scope: Scope) { - 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) + } } }