From 5ccab84aa4c2432f61b4893aab1f9a628bb59144 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Sat, 18 Apr 2026 20:53:54 +0800 Subject: [PATCH] perf: cache sorted visible key names on Val.Obj Motivation: Non-inline objects (>8 fields, super chains, excludedKeys) call .sorted(CodepointStringOrdering) on visibleKeyNames at every materialization. This re-sorts and allocates a new array each time. Modification: Add _sortedVisibleKeyNames cache on Val.Obj with lazy initialization. Replace all 5 call sites (Materializer recursive/stackless, Renderer, ByteRenderer, Val.Obj.foreachElement) with the cached accessor. Result: Objects with stable schemas sort their key names once and reuse the result on subsequent materializations. --- sjsonnet/src/sjsonnet/ByteRenderer.scala | 2 +- sjsonnet/src/sjsonnet/Materializer.scala | 4 ++-- sjsonnet/src/sjsonnet/Val.scala | 15 +++++++++++++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/sjsonnet/src/sjsonnet/ByteRenderer.scala b/sjsonnet/src/sjsonnet/ByteRenderer.scala index 7eb8656f..2287f641 100644 --- a/sjsonnet/src/sjsonnet/ByteRenderer.scala +++ b/sjsonnet/src/sjsonnet/ByteRenderer.scala @@ -391,7 +391,7 @@ class ByteRenderer(out: OutputStream = new java.io.ByteArrayOutputStream(), inde matDepth: Int, ctx: Materializer.MaterializeContext)(implicit evaluator: EvalScope): Unit = { val keys = - if (ctx.sort) obj.visibleKeyNames.sorted(Util.CodepointStringOrdering) + if (ctx.sort) obj.sortedVisibleKeyNames else obj.visibleKeyNames openObjBrace() diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala index 0742ca6e..d184a9e0 100644 --- a/sjsonnet/src/sjsonnet/Materializer.scala +++ b/sjsonnet/src/sjsonnet/Materializer.scala @@ -100,7 +100,7 @@ abstract class Materializer { else materializeInlineObj(obj, visitor, depth, ctx) } else { val keys = - if (ctx.sort) obj.visibleKeyNames.sorted(Util.CodepointStringOrdering) + if (ctx.sort) obj.sortedVisibleKeyNames else obj.visibleKeyNames val ov = visitor.visitObject(keys.length, jsonableKeys = true, -1) var i = 0 @@ -432,7 +432,7 @@ abstract class Materializer { storePos(obj.pos) obj.triggerAllAsserts(ctx.brokenAssertionLogic) val keyNames = - if (ctx.sort) obj.visibleKeyNames.sorted(Util.CodepointStringOrdering) + if (ctx.sort) obj.sortedVisibleKeyNames else obj.visibleKeyNames val objVisitor = visitor.visitObject(keyNames.length, jsonableKeys = true, -1) stack.push( diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index 4b5d7633..af4acbaa 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -884,7 +884,18 @@ object Val { * Cached sorted field order for inline objects. Shared across all objects from the same * MemberList to avoid per-object sort + allocation. */ - @volatile private[sjsonnet] var _sortedInlineOrder: Array[Int] = null + private[sjsonnet] var _sortedInlineOrder: Array[Int] = null + + private[sjsonnet] var _sortedVisibleKeyNames: Array[String] = null + + private[sjsonnet] def sortedVisibleKeyNames: Array[String] = { + var r = _sortedVisibleKeyNames + if (r == null) { + r = visibleKeyNames.sorted(Util.CodepointStringOrdering) + _sortedVisibleKeyNames = r + } + r + } /** * When true, field caching can be skipped during materialization because no field body @@ -1402,7 +1413,7 @@ object Val { def foreachElement(sort: Boolean, pos: Position)(f: (String, Val) => Unit)(implicit ev: EvalScope): Unit = { - val keys = if (sort) visibleKeyNames.sorted(Util.CodepointStringOrdering) else visibleKeyNames + val keys = if (sort) sortedVisibleKeyNames else visibleKeyNames for (k <- keys) { val v = value(k, pos) f(k, v)