Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions sjsonnet/src/sjsonnet/Renderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,41 @@ import java.io.{StringWriter, Writer}

import upickle.core.{ArrVisitor, ObjVisitor}

final class StringBuilderWriter(initialCapacity: Int = 16) extends Writer {
private[this] val builder = new java.lang.StringBuilder(initialCapacity)

override def write(c: Int): Unit =
builder.append(c.toChar)

override def write(cbuf: Array[Char], off: Int, len: Int): Unit =
builder.append(cbuf, off, len)

override def write(str: String): Unit =
builder.append(str)

override def write(str: String, off: Int, len: Int): Unit =
builder.append(str, off, off + len)

override def append(c: Char): Writer = {
builder.append(c)
this
}

override def append(csq: CharSequence): Writer = {
builder.append(if (csq == null) "null" else csq)
this
}

override def append(csq: CharSequence, start: Int, end: Int): Writer = {
builder.append(if (csq == null) "null" else csq, start, end)
this
}

override def flush(): Unit = ()
override def close(): Unit = ()
override def toString: String = builder.toString
}

/**
* Custom JSON renderer to try and match the behavior of google/jsonnet's render:
*
Expand Down Expand Up @@ -279,6 +314,93 @@ final case class MaterializeJsonRenderer(
}
}

private[sjsonnet] final class FastMaterializeJsonRenderer(
indent: Int = 4,
escapeUnicode: Boolean = false,
newline: String = "\n",
keyValueSeparator: String = ": ",
private val outWriter: StringBuilderWriter = new StringBuilderWriter())
extends BaseCharRenderer(
outWriter,
indent,
escapeUnicode,
newline.toCharArray
) {
private val newLineCharArray = newline.toCharArray
private val keyValueSeparatorCharArray = keyValueSeparator.toCharArray

private val reusableArrVisitor: ArrVisitor[StringBuilderWriter, StringBuilderWriter] {
def subVisitor: sjsonnet.FastMaterializeJsonRenderer
} = new ArrVisitor[StringBuilderWriter, StringBuilderWriter] {
def subVisitor: sjsonnet.FastMaterializeJsonRenderer = FastMaterializeJsonRenderer.this
def visitValue(v: StringBuilderWriter, index: Int): Unit = {
flushBuffer()
commaBuffered = true
}
def visitEnd(index: Int): StringBuilderWriter = {
commaBuffered = false
depth -= 1
renderIndent()
elemBuilder.append(']')
flushCharBuilder()
outWriter
}
}

private val reusableObjVisitor: ObjVisitor[StringBuilderWriter, StringBuilderWriter] {
def subVisitor: sjsonnet.FastMaterializeJsonRenderer
def visitKey(index: Int): sjsonnet.FastMaterializeJsonRenderer
} = new ObjVisitor[StringBuilderWriter, StringBuilderWriter] {
def subVisitor: sjsonnet.FastMaterializeJsonRenderer = FastMaterializeJsonRenderer.this
def visitKey(index: Int): sjsonnet.FastMaterializeJsonRenderer =
FastMaterializeJsonRenderer.this
def visitKeyValue(s: Any): Unit = {
elemBuilder.appendAll(keyValueSeparatorCharArray, keyValueSeparatorCharArray.length)
}
def visitValue(v: StringBuilderWriter, index: Int): Unit = {
commaBuffered = true
}
def visitEnd(index: Int): StringBuilderWriter = {
commaBuffered = false
depth -= 1
renderIndent()
elemBuilder.append('}')
flushCharBuilder()
outWriter
}
}

override def visitArray(
length: Int,
index: Int): upickle.core.ArrVisitor[StringBuilderWriter, StringBuilderWriter] {
def subVisitor: sjsonnet.FastMaterializeJsonRenderer
} = {
flushBuffer()
elemBuilder.append('[')

depth += 1
if (length == 0 && indent != -1)
elemBuilder.appendAll(newLineCharArray, newLineCharArray.length)
else renderIndent()
reusableArrVisitor
}

override def visitObject(
length: Int,
index: Int): upickle.core.ObjVisitor[StringBuilderWriter, StringBuilderWriter] {
def subVisitor: sjsonnet.FastMaterializeJsonRenderer
def visitKey(index: Int): sjsonnet.FastMaterializeJsonRenderer
} = {
flushBuffer()
elemBuilder.append('{')
depth += 1
if (length == 0 && indent != -1)
elemBuilder.appendAll(newLineCharArray, newLineCharArray.length)
else renderIndent()
reusableObjVisitor
}
}

object RenderUtils {

// Pre-cached string representations of small integers (0-255)
Expand Down
54 changes: 29 additions & 25 deletions sjsonnet/src/sjsonnet/TomlRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ package sjsonnet

import upickle.core.{ArrVisitor, ObjVisitor, SimpleVisitor, Visitor}

import java.io.StringWriter

// Uses the unsynchronized [[StringBuilderWriter]] rather than java.io.StringWriter: the latter is
// backed by a synchronized StringBuffer, paying a monitor enter/exit on every write/flush on the
// hot manifestTomlEx path. Output is byte-identical. Same swap as the JSON renderer in #874.
class TomlRenderer(
out: StringWriter = new java.io.StringWriter(),
out: StringBuilderWriter = new StringBuilderWriter(),
cumulatedIndent: String,
indent: String)
extends SimpleVisitor[StringWriter, StringWriter] {
extends SimpleVisitor[StringBuilderWriter, StringBuilderWriter] {
override def expectedMsg: String = "unimplemented type in Materializer"
private object objectKeyRenderer extends upickle.core.SimpleVisitor[StringWriter, StringWriter] {
private object objectKeyRenderer
extends upickle.core.SimpleVisitor[StringBuilderWriter, StringBuilderWriter] {
override def expectedMsg = "expected string"

override def visitNull(index: Int): StringWriter = {
override def visitNull(index: Int): StringBuilderWriter = {
TomlRenderer.this.visitNull(index)
}

override def visitString(s: CharSequence, index: Int): StringWriter = {
override def visitString(s: CharSequence, index: Int): StringBuilderWriter = {
if (s == null) visitNull(index)
else {
TomlRenderer.writeEscapedKey(out, s)
Expand All @@ -33,19 +35,19 @@ class TomlRenderer(
out
}

override def visitNull(index: Int): StringWriter = Error.fail("Tried to manifest \"null\"")
override def visitNull(index: Int): StringBuilderWriter = Error.fail("Tried to manifest \"null\"")

override def visitTrue(index: Int): StringWriter = {
override def visitTrue(index: Int): StringBuilderWriter = {
out.write("true")
flush
}

override def visitFalse(index: Int): StringWriter = {
override def visitFalse(index: Int): StringBuilderWriter = {
out.write("false")
flush
}

override def visitString(s: CharSequence, index: Int): StringWriter = {
override def visitString(s: CharSequence, index: Int): StringBuilderWriter = {
if (s == null) {
visitNull(index)
} else {
Expand All @@ -54,7 +56,7 @@ class TomlRenderer(
}
}

override def visitFloat64(d: Double, index: Int): StringWriter = {
override def visitFloat64(d: Double, index: Int): StringBuilderWriter = {
d match {
case Double.PositiveInfinity => out.write("inf")
case Double.NegativeInfinity => out.write("-inf")
Expand All @@ -65,8 +67,10 @@ class TomlRenderer(
flush
}

override def visitArray(length: Int, index: Int): ArrVisitor[StringWriter, StringWriter] =
new ArrVisitor[StringWriter, StringWriter] {
override def visitArray(
length: Int,
index: Int): ArrVisitor[StringBuilderWriter, StringBuilderWriter] =
new ArrVisitor[StringBuilderWriter, StringBuilderWriter] {
private val isInLine = length == 0 || depth > 0
private val newElementIndent = if (isInLine) "" else cumulatedIndent + indent
private val separator =
Expand All @@ -76,18 +80,18 @@ class TomlRenderer(
depth += 1
out.write('[')
out.write(separator)
def subVisitor: Visitor[StringWriter, StringWriter] = {
def subVisitor: Visitor[StringBuilderWriter, StringBuilderWriter] = {
if (addComma) {
out.write(',')
out.write(separator)
}
out.write(newElementIndent)
TomlRenderer.this
}
def visitValue(v: StringWriter, index: Int): Unit = {
def visitValue(v: StringBuilderWriter, index: Int): Unit = {
addComma = true
}
def visitEnd(index: Int): StringWriter = {
def visitEnd(index: Int): StringBuilderWriter = {
addComma = false
depth -= 1
out.write(separator)
Expand All @@ -100,23 +104,23 @@ class TomlRenderer(
override def visitObject(
length: Int,
jsonableKeys: Boolean,
index: Int): ObjVisitor[StringWriter, StringWriter] =
new ObjVisitor[StringWriter, StringWriter] {
index: Int): ObjVisitor[StringBuilderWriter, StringBuilderWriter] =
new ObjVisitor[StringBuilderWriter, StringBuilderWriter] {
private var addComma = false
depth += 1
out.write("{ ")
def subVisitor: Visitor[StringWriter, StringWriter] = TomlRenderer.this
def visitKey(index: Int): Visitor[StringWriter, StringWriter] = {
def subVisitor: Visitor[StringBuilderWriter, StringBuilderWriter] = TomlRenderer.this
def visitKey(index: Int): Visitor[StringBuilderWriter, StringBuilderWriter] = {
if (addComma) out.write(", ")
objectKeyRenderer
}
def visitKeyValue(s: Any): Unit = {
out.write(" = ")
}
def visitValue(v: StringWriter, index: Int): Unit = {
def visitValue(v: StringBuilderWriter, index: Int): Unit = {
addComma = true
}
def visitEnd(index: Int): StringWriter = {
def visitEnd(index: Int): StringBuilderWriter = {
addComma = false
depth -= 1
out.write(" }")
Expand Down Expand Up @@ -146,14 +150,14 @@ object TomlRenderer {
}
}

def writeEscapedKey(out: StringWriter, key: CharSequence): Unit = {
def writeEscapedKey(out: StringBuilderWriter, key: CharSequence): Unit = {
if (isBareKey(key)) out.write(key.toString)
else BaseRenderer.escape(out, key, unicode = true)
}

def escapeKey(key: String): String = if (isBareKey(key)) key
else {
val out = new StringWriter()
val out = new StringBuilderWriter()
writeEscapedKey(out, key)
out.toString
}
Expand Down
12 changes: 8 additions & 4 deletions sjsonnet/src/sjsonnet/Util.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,10 @@ object Util {
while (i1 < n1 && i2 < n2) {
val c1 = s1.charAt(i1)
val c2 = s2.charAt(i2)
// Fast path: equal chars can be skipped without surrogate checks.
// Even for surrogate pairs, equal high surrogates at position i lead to
// comparing low surrogates at i+1, producing the correct codepoint ordering.
if (c1 == c2) {
// Fast path: equal non-surrogates can be skipped without codepoint checks.
// Equal surrogates still need codepoint decoding because a raw surrogate and
// a valid surrogate pair can share the same leading UTF-16 code unit.
if (c1 == c2 && !Character.isSurrogate(c1)) {
i1 += 1
i2 += 1
} else if (!Character.isSurrogate(c1) && !Character.isSurrogate(c2)) {
Expand All @@ -157,6 +157,10 @@ object Util {
override def compare(x: String, y: String): Int = compareStringsByCodepoint(x, y)
}

def sortStringsByCodepointInPlace(xs: Array[String]): Unit = {
java.util.Arrays.sort(xs, CodepointStringOrdering)
}

def compareJsonnetStd(v1: Val, v2: Val, ev: EvalScope): Int = {
val t1 = v1.prettyName
val t2 = v2.prettyName
Expand Down
3 changes: 2 additions & 1 deletion sjsonnet/src/sjsonnet/Val.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1822,7 +1822,8 @@ object Val {
private[sjsonnet] def sortedVisibleKeyNames: Array[String] = {
var r = _sortedVisibleKeyNames
if (r == null) {
r = visibleKeyNames.sorted(Util.CodepointStringOrdering)
r = visibleKeyNames.clone()
Util.sortStringsByCodepointInPlace(r)
_sortedVisibleKeyNames = r
}
r
Expand Down
18 changes: 10 additions & 8 deletions sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ object ManifestModule extends AbstractFunctionModule {
*/
private object ManifestJson extends Val.Builtin1("manifestJson", "v") {
def evalRhs(v: Eval, ev: EvalScope, pos: Position): Val =
Val.Str(pos, Materializer.apply0(v.value, MaterializeJsonRenderer())(ev).toString)
Val.Str(pos, Materializer.apply0(v.value, new FastMaterializeJsonRenderer())(ev).toString)
}

/**
Expand All @@ -57,7 +57,7 @@ object ManifestModule extends AbstractFunctionModule {
Materializer
.apply0(
v.value,
MaterializeJsonRenderer(indent = -1, newline = "", keyValueSeparator = ":")
new FastMaterializeJsonRenderer(indent = -1, newline = "", keyValueSeparator = ":")
)(ev)
.toString
)
Expand Down Expand Up @@ -94,7 +94,7 @@ object ManifestModule extends AbstractFunctionModule {
Materializer
.apply0(
v.value,
MaterializeJsonRenderer(
new FastMaterializeJsonRenderer(
indent = i.value.asString.length,
newline = newline.value.asString,
keyValueSeparator = keyValSep.value.asString
Expand Down Expand Up @@ -184,11 +184,11 @@ object ManifestModule extends AbstractFunctionModule {
}

private def renderTableInternal(
out: StringWriter,
out: StringBuilderWriter,
v: Val.Obj,
cumulatedIndent: String,
indent: String,
path: mutable.ArrayBuffer[String])(implicit ev: EvalScope): StringWriter = {
path: mutable.ArrayBuffer[String])(implicit ev: EvalScope): StringBuilderWriter = {
val keys = v.sortedVisibleKeyNames
if (keys.length == 0) {
out.write('\n')
Expand Down Expand Up @@ -263,7 +263,7 @@ object ManifestModule extends AbstractFunctionModule {
out
}

private def renderTableHeader(out: StringWriter, path: mutable.ArrayBuffer[String]) = {
private def renderTableHeader(out: StringBuilderWriter, path: mutable.ArrayBuffer[String]) = {
out.write('[')
var i = 0
while (i < path.length) {
Expand All @@ -275,15 +275,17 @@ object ManifestModule extends AbstractFunctionModule {
out
}

private def renderTableArrayHeader(out: StringWriter, path: mutable.ArrayBuffer[String]) = {
private def renderTableArrayHeader(
out: StringBuilderWriter,
path: mutable.ArrayBuffer[String]) = {
out.write('[')
renderTableHeader(out, path)
out.write(']')
out
}

def evalRhs(v: Eval, indent: Eval, ev: EvalScope, pos: Position): Val = {
val out = new StringWriter
val out = new StringBuilderWriter
renderTableInternal(
out,
v.value.asObj,
Expand Down
Loading
Loading