Skip to content

Commit 4a1c1f0

Browse files
committed
perf: lazy stdlib initialization to reduce startup overhead
Motivation: Startup with empty `{}` showed parse_time=3.1ms due to eager construction of 140+ Val.Builtin objects across 11 modules. Most programs only use a small subset of stdlib functions, wasting allocation and init time. Modification: - Add `final class LazyConstMember` to `Val.Obj` — defers builtin creation until first access via a `() => Val` thunk, with null-check fast path. - Add `functionNames: Array[String]` to `AbstractFunctionModule` — cheap name-only registration at startup with zero Val.Func allocation. - Add `getFunction(name)` to `AbstractFunctionModule` — lazy per-module lookup that triggers module initialization on first access. - Change `functions` from eager `val` to `lazy val` in all 11 modules (ArrayModule, StringModule, ObjectModule, MathModule, TypeModule, EncodingModule, ManifestModule, SetModule, NativeRegex, NativeGzip, NativeXz). - Rewrite `StdLibModule` to build the std object with `LazyConstMember` entries backed by a `nameToModule` index (string→module), replacing the eager `allModuleFunctions` aggregation. Result: parse_time for empty `{}` drops from 3.1ms to ~60μs (50x improvement). Wall-clock startup (hyperfine) drops from ~5.9ms to ~4.4ms (-25%). No impact on steady-state performance — lazy init is one-time per module.
1 parent ef04cb4 commit 4a1c1f0

14 files changed

Lines changed: 290 additions & 46 deletions

File tree

sjsonnet/src-jvm-native/sjsonnet/stdlib/NativeGzip.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val}
66
object NativeGzip extends AbstractFunctionModule {
77
def name = "gzip"
88

9-
val functions: Seq[(String, Val.Builtin)] = Seq(
9+
val functionNames: Array[String] = Array("gzip")
10+
11+
lazy val functions: Seq[(String, Val.Builtin)] = Seq(
1012
"gzip" -> new Val.Builtin1("gzip", "v") {
1113
override def evalRhs(v: Eval, ev: EvalScope, pos: Position): Val = v.value match {
1214
case Val.Str(_, value) => Val.Str(pos, Platform.gzipString(value))

sjsonnet/src-jvm/sjsonnet/stdlib/NativeXz.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val}
66
object NativeXz extends AbstractFunctionModule {
77
def name = "xz"
88

9-
val functions: Seq[(String, Val.Builtin)] = Seq(
9+
val functionNames: Array[String] = Array("xz")
10+
11+
lazy val functions: Seq[(String, Val.Builtin)] = Seq(
1012
"xz" -> new Val.Builtin2(
1113
"xz",
1214
"v",

sjsonnet/src/sjsonnet/Val.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,20 @@ object Val {
556556
def invoke(self: Obj, sup: Obj, fs: FileScope, ev: EvalScope): Val = v
557557
}
558558

559+
/**
560+
* A member whose value is created lazily on first access. Used by stdlib to defer builtin
561+
* function instantiation until actually needed, reducing startup overhead.
562+
*/
563+
final class LazyConstMember(add2: Boolean, visibility2: Visibility, init: () => Val)
564+
extends Member(add2, visibility2, cached = true, deprecatedSkipAsserts = true) {
565+
private var _val: Val = _
566+
def invoke(self: Obj, sup: Obj, fs: FileScope, ev: EvalScope): Val = {
567+
var v = _val
568+
if (v eq null) { v = init(); _val = v }
569+
v
570+
}
571+
}
572+
559573
def mk(pos: Position, members: (String, Obj.Member)*): Obj = {
560574
val m = Util.preSizedJavaLinkedHashMap[String, Obj.Member](members.length)
561575
for ((k, v) <- members) m.put(k, v)

sjsonnet/src/sjsonnet/functions/AbstractFunctionModule.scala

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,27 @@ import sjsonnet.Val
88
abstract class AbstractFunctionModule extends FunctionModule {
99

1010
/**
11-
* module's functions
11+
* Module function names — cheap string array for lazy registration. No Val.Func objects created.
1212
*/
13-
val functions: Seq[(String, Val.Func)]
13+
def functionNames: Array[String]
1414

1515
/**
16-
* the hodler of the module object
16+
* module's functions — override as lazy val in concrete modules to defer Val.Builtin creation.
17+
*/
18+
def functions: Seq[(String, Val.Func)]
19+
20+
/**
21+
* Lazy function lookup map, built on first access to any function in this module.
22+
*/
23+
private lazy val functionMap: Map[String, Val.Func] = functions.toMap
24+
25+
/**
26+
* Get a function by name, triggering module initialization if needed.
27+
*/
28+
def getFunction(name: String): Val.Func = functionMap(name)
29+
30+
/**
31+
* the holder of the module object
1732
*/
1833
override final lazy val module: Val.Obj = moduleFromFunctions(functions: _*)
1934
}

sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,35 @@ object ArrayModule extends AbstractFunctionModule {
433433
Val.Arr(pos, chars)
434434
}
435435

436-
val functions: Seq[(String, Val.Func)] = Seq(
436+
val functionNames: Array[String] = Array(
437+
"minArray",
438+
"maxArray",
439+
"all",
440+
"any",
441+
"count",
442+
"filter",
443+
"map",
444+
"mapWithIndex",
445+
"find",
446+
"flattenArrays",
447+
"flattenDeepArray",
448+
"reverse",
449+
"member",
450+
"range",
451+
"foldl",
452+
"foldr",
453+
"flatMap",
454+
"filterMap",
455+
"repeat",
456+
"makeArray",
457+
"contains",
458+
"remove",
459+
"removeAt",
460+
"sum",
461+
"avg"
462+
)
463+
464+
lazy val functions: Seq[(String, Val.Func)] = Seq(
437465
builtin(MinArray),
438466
builtin(MaxArray),
439467
builtin(All),

sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,18 @@ object EncodingModule extends AbstractFunctionModule {
1313
Val.Str(pos, Platform.md5(s.value.asString))
1414
}
1515

16-
val functions: Seq[(String, Val.Func)] = Seq(
16+
val functionNames: Array[String] = Array(
17+
"md5",
18+
"base64",
19+
"base64Decode",
20+
"base64DecodeBytes",
21+
"sha1",
22+
"sha256",
23+
"sha512",
24+
"sha3"
25+
)
26+
27+
lazy val functions: Seq[(String, Val.Func)] = Seq(
1728
builtin(MD5),
1829
builtin("base64", "input") { (_, _, input: Val) =>
1930
input match {

sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,25 @@ object ManifestModule extends AbstractFunctionModule {
209209
}
210210
}
211211

212-
val functions: Seq[(String, Val.Func)] = Seq(
212+
val functionNames: Array[String] = Array(
213+
"manifestJson",
214+
"manifestJsonMinified",
215+
"manifestJsonEx",
216+
"parseJson",
217+
"parseYaml",
218+
"manifestTomlEx",
219+
"lines",
220+
"deepJoin",
221+
"manifestToml",
222+
"manifestYamlDoc",
223+
"manifestYamlStream",
224+
"manifestIni",
225+
"manifestPython",
226+
"manifestPythonVars",
227+
"manifestXmlJsonml"
228+
)
229+
230+
lazy val functions: Seq[(String, Val.Func)] = Seq(
213231
builtin(ManifestJson),
214232
builtin(ManifestJsonMinified),
215233
builtin(ManifestJsonEx),

sjsonnet/src/sjsonnet/stdlib/MathModule.scala

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,44 @@ import sjsonnet.functions.AbstractFunctionModule
66
object MathModule extends AbstractFunctionModule {
77
def name = "math"
88

9-
val functions: Seq[(String, Val.Func)] = Seq(
9+
val functionNames: Array[String] = Array(
10+
"sqrt",
11+
"max",
12+
"min",
13+
"mod",
14+
"modulo",
15+
"clamp",
16+
"pow",
17+
"floor",
18+
"round",
19+
"ceil",
20+
"abs",
21+
"sign",
22+
"sin",
23+
"cos",
24+
"tan",
25+
"isEven",
26+
"isInteger",
27+
"isOdd",
28+
"isDecimal",
29+
"asin",
30+
"acos",
31+
"atan",
32+
"atan2",
33+
"hypot",
34+
"deg2rad",
35+
"rad2deg",
36+
"log",
37+
"log2",
38+
"log10",
39+
"exp",
40+
"mantissa",
41+
"exponent",
42+
"xor",
43+
"xnor"
44+
)
45+
46+
lazy val functions: Seq[(String, Val.Func)] = Seq(
1047
builtin("sqrt", "x") { (pos, ev, x: Double) =>
1148
math.sqrt(x)
1249
},

sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,15 @@ object NativeRegex extends AbstractFunctionModule {
4343
}
4444
}
4545

46-
val functions: Seq[(String, Val.Builtin)] = Seq(
46+
val functionNames: Array[String] = Array(
47+
"regexPartialMatch",
48+
"regexFullMatch",
49+
"regexGlobalReplace",
50+
"regexReplace",
51+
"regexQuoteMeta"
52+
)
53+
54+
lazy val functions: Seq[(String, Val.Builtin)] = Seq(
4755
"regexPartialMatch" -> new Val.Builtin2("regexPartialMatch", "pattern", "str") {
4856
override def evalRhs(pattern: Eval, str: Eval, ev: EvalScope, pos: Position): Val = {
4957
regexPartialMatch(pos, pattern.value.asString, str.value.asString)

sjsonnet/src/sjsonnet/stdlib/ObjectModule.scala

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,25 @@ object ObjectModule extends AbstractFunctionModule {
173173
}
174174
)
175175

176-
val functions: Seq[(String, Val.Func)] = Seq(
176+
val functionNames: Array[String] = Array(
177+
"objectHas",
178+
"objectHasAll",
179+
"objectHasEx",
180+
"objectFields",
181+
"objectFieldsAll",
182+
"objectFieldsEx",
183+
"objectValues",
184+
"objectValuesAll",
185+
"get",
186+
"mapWithKey",
187+
"objectKeysValues",
188+
"objectKeysValuesAll",
189+
"objectRemoveKey",
190+
"mergePatch",
191+
"prune"
192+
)
193+
194+
lazy val functions: Seq[(String, Val.Func)] = Seq(
177195
builtin(ObjectHas),
178196
builtin(ObjectHasAll),
179197
builtin("objectHasEx", "o", "k", "inc_hidden") {

0 commit comments

Comments
 (0)