Skip to content

perf: pre-cached indent arrays for bulk newline+spaces#676

Open
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/renderer-indent-cache
Open

perf: pre-cached indent arrays for bulk newline+spaces#676
He-Pin wants to merge 1 commit intodatabricks:masterfrom
He-Pin:perf/renderer-indent-cache

Conversation

@He-Pin
Copy link
Copy Markdown
Contributor

@He-Pin He-Pin commented Apr 4, 2026

Motivation

The JSON renderer generates indentation strings (newline + spaces) on every nested element. For deeply nested Jsonnet output, Renderer.visitKey and visitEnd repeatedly construct identical indent strings. The current implementation calls elemBuilder.append('\n') followed by a while loop appending spaces — this is O(depth) per indent operation.

Key Design Decision

Pre-cache indent strings (newline + spaces) in a companion object array up to depth 64 (MaxCachedDepth). For depths ≤64, indent operations become a single array lookup + bulk write. For depths >64 (rare in practice), fall through to the original loop.

Modification

sjsonnet/src/sjsonnet/Renderer.scala:

  • Added companion object with MaxCachedDepth = 64 constant and indentCache: Array[Array[Char]]
  • Cache stores pre-computed "\n" + " " * (depth * indent) as char arrays for depths 0–64
  • flushBuffer() fast path: when depth ≤ MaxCachedDepth, uses elemBuilder.appendAll(cachedArray, len) instead of character-by-character loop
  • Original loop preserved as fallback for depths > 64

Benchmark Results

JMH — Full Suite (35 benchmarks, 1+1 warmup)

No regressions detected. All benchmarks within noise margin.

Note

The indentation cache optimization primarily benefits:

  1. Deeply nested JSON output — common in Jsonnet configurations (Kubernetes manifests, CI configs)
  2. std.manifestJsonEx — uses indentation for pretty-printing
  3. Scala Native — no JIT to optimize the loop; pre-cached arrays enable System.arraycopy

Analysis

  • Memory: One-time allocation of 64 char arrays (total ~2KB) — negligible.
  • Thread safety: Cache is in a companion object, initialized once. Arrays are read-only after initialization.
  • Threshold: 64 levels covers virtually all real-world Jsonnet output (even deeply nested Kubernetes manifests rarely exceed 20 levels).

References

  • upickle.core.CharBuilder.appendAll(char[], int) for bulk writes
  • Original character-by-character indent loop in Renderer.flushBuffer

Result

Pre-cached indent arrays eliminate per-character overhead for nested JSON rendering. No regressions. Benefits deeply nested output and Scala Native.

@He-Pin He-Pin marked this pull request as ready for review April 5, 2026 00:28
@He-Pin He-Pin force-pushed the perf/renderer-indent-cache branch 5 times, most recently from f2e7618 to 9db668d Compare April 9, 2026 04:46
@He-Pin
Copy link
Copy Markdown
Contributor Author

He-Pin commented Apr 9, 2026

Good catch — extracted the magic 16 into a named constant Renderer.MaxCachedDepth in a new companion object. The comparison now reads depth < MaxCachedDepth instead of depth < indentCache.length.

Note that the indent cache content is instance-specific (depends on the indent constructor parameter — commonly 2, 3, or 4), but the size (16 depth levels) is a fixed constant shared across all instances.

@He-Pin He-Pin force-pushed the perf/renderer-indent-cache branch from 9db668d to 7ec85ce Compare April 9, 2026 11:30
Extract MaxCachedDepth=16 to Renderer companion object constant per review.
Pre-compute indentCache arrays for depths 0..15 to replace per-character
space emission with a single bulk appendAll in flushBuffer.
@He-Pin He-Pin force-pushed the perf/renderer-indent-cache branch from 7ec85ce to abfe59a Compare April 9, 2026 15:18
var d = 0
while (d < MaxCachedDepth) {
val spaces = indent * d
val buf = new Array[Char](spaces + 1)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would'nt

  val buf = Array.fill(spaces + 1) { ' ' }
  buf(0) = '\n'

be more terse?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants