Skip to content

Commit b48ebfc

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 0d13274 commit b48ebfc

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
@@ -635,6 +635,20 @@ object Val {
635635
def invoke(self: Obj, sup: Obj, fs: FileScope, ev: EvalScope): Val = v
636636
}
637637

638+
/**
639+
* A member whose value is created lazily on first access. Used by stdlib to defer builtin
640+
* function instantiation until actually needed, reducing startup overhead.
641+
*/
642+
final class LazyConstMember(add2: Boolean, visibility2: Visibility, init: () => Val)
643+
extends Member(add2, visibility2, cached = true, deprecatedSkipAsserts = true) {
644+
private var _val: Val = _
645+
def invoke(self: Obj, sup: Obj, fs: FileScope, ev: EvalScope): Val = {
646+
var v = _val
647+
if (v eq null) { v = init(); _val = v }
648+
v
649+
}
650+
}
651+
638652
def mk(pos: Position, members: (String, Obj.Member)*): Obj = {
639653
val m = Util.preSizedJavaLinkedHashMap[String, Obj.Member](members.length)
640654
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
@@ -459,7 +459,35 @@ object ArrayModule extends AbstractFunctionModule {
459459
Val.Arr(pos, chars)
460460
}
461461

462-
val functions: Seq[(String, Val.Func)] = Seq(
462+
val functionNames: Array[String] = Array(
463+
"minArray",
464+
"maxArray",
465+
"all",
466+
"any",
467+
"count",
468+
"filter",
469+
"map",
470+
"mapWithIndex",
471+
"find",
472+
"flattenArrays",
473+
"flattenDeepArray",
474+
"reverse",
475+
"member",
476+
"range",
477+
"foldl",
478+
"foldr",
479+
"flatMap",
480+
"filterMap",
481+
"repeat",
482+
"makeArray",
483+
"contains",
484+
"remove",
485+
"removeAt",
486+
"sum",
487+
"avg"
488+
)
489+
490+
lazy val functions: Seq[(String, Val.Func)] = Seq(
463491
builtin(MinArray),
464492
builtin(MaxArray),
465493
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
@@ -194,7 +194,25 @@ object ObjectModule extends AbstractFunctionModule {
194194
Val.Arr(pos, result)
195195
}
196196

197-
val functions: Seq[(String, Val.Func)] = Seq(
197+
val functionNames: Array[String] = Array(
198+
"objectHas",
199+
"objectHasAll",
200+
"objectHasEx",
201+
"objectFields",
202+
"objectFieldsAll",
203+
"objectFieldsEx",
204+
"objectValues",
205+
"objectValuesAll",
206+
"get",
207+
"mapWithKey",
208+
"objectKeysValues",
209+
"objectKeysValuesAll",
210+
"objectRemoveKey",
211+
"mergePatch",
212+
"prune"
213+
)
214+
215+
lazy val functions: Seq[(String, Val.Func)] = Seq(
198216
builtin(ObjectHas),
199217
builtin(ObjectHasAll),
200218
builtin("objectHasEx", "o", "k", "inc_hidden") {

0 commit comments

Comments
 (0)