Skip to content
Open
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
48 changes: 35 additions & 13 deletions sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package sjsonnet
import upickle.core.SimpleVisitor

import java.io.{
BufferedOutputStream,
InputStream,
OutputStreamWriter,
PrintStream,
Expand Down Expand Up @@ -170,7 +169,8 @@ object SjsonnetMainBase {
warn,
std,
debugStats = debugStats,
profileOpt = config.profile
profileOpt = config.profile,
stdout = if (config.outputFile.isEmpty) stdout else null
)
res <- {
if (hasWarnings && config.fatalWarnings.value) Left("")
Expand Down Expand Up @@ -236,9 +236,25 @@ object SjsonnetMainBase {
)
)

private def writeToFile(config: Config, wd: os.Path)(
private def writeToFile(config: Config, wd: os.Path, stdout: PrintStream = null)(
materialize: Writer => Either[String, ?]): Either[String, String] = {
config.outputFile match {
case None if stdout != null =>
// Direct-write mode: render into an unsynchronized in-memory buffer, then
// flush to stdout in a single writeTo() call. This eliminates ~thousands of
// synchronized write calls through ByteArrayOutputStream — the only
// synchronization point is the final bulk write to stdout.
// On rendering error, the buffer is simply discarded (nothing reaches stdout).
val baos = new UnsynchronizedByteArrayOutputStream(65536)
val wr = new OutputStreamWriter(baos, StandardCharsets.UTF_8)
val u = materialize(wr)
u.map { _ =>
if (!config.noTrailingNewline.value) wr.write('\n')
wr.flush()
baos.writeTo(stdout)
stdout.flush()
""
}
case None =>
val sw = new StringWriter
materialize(sw).map(_ => sw.toString)
Expand All @@ -247,11 +263,14 @@ object SjsonnetMainBase {
os.write.over.outputStream(os.Path(f, wd), createFolders = config.createDirs.value)
).flatMap { out =>
try {
val buf = new BufferedOutputStream(out)
val wr = new OutputStreamWriter(buf, StandardCharsets.UTF_8)
val baos = new UnsynchronizedByteArrayOutputStream(65536)
val wr = new OutputStreamWriter(baos, StandardCharsets.UTF_8)
val u = materialize(wr)
wr.flush()
u.map(_ => "")
u.map { _ =>
wr.flush()
baos.writeTo(out)
""
}
} finally out.close()
}
}
Expand All @@ -263,8 +282,9 @@ object SjsonnetMainBase {
jsonnetCode: String,
path: os.Path,
wd: os.Path,
getCurrentPosition: () => Position) = {
writeToFile(config, wd) { writer =>
getCurrentPosition: () => Position,
stdout: PrintStream = null) = {
writeToFile(config, wd, stdout) { writer =>
val renderer = rendererForConfig(writer, config, getCurrentPosition)
val res = interp.interpret0(jsonnetCode, OsPath(path), renderer)
if (config.yamlOut.value && !config.noTrailingNewline.value) writer.write('\n')
Expand Down Expand Up @@ -320,7 +340,8 @@ object SjsonnetMainBase {
std: Val.Obj,
evaluatorOverride: Option[Evaluator] = None,
debugStats: DebugStats = null,
profileOpt: Option[String] = None): Either[String, String] = {
profileOpt: Option[String] = None,
stdout: PrintStream = null): Either[String, String] = {

val (jsonnetCode, path) =
if (config.exec.value) (file, wd / Util.wrapInLessThanGreaterThan("exec"))
Expand Down Expand Up @@ -431,7 +452,7 @@ object SjsonnetMainBase {

interp.interpret(jsonnetCode, OsPath(path)).flatMap {
case arr: ujson.Arr =>
writeToFile(config, wd) { writer =>
writeToFile(config, wd, stdout) { writer =>
arr.value.toSeq match {
case Nil => // donothing
case Seq(single) =>
Expand All @@ -455,9 +476,10 @@ object SjsonnetMainBase {
Right("")
}

case _ => renderNormal(config, interp, jsonnetCode, path, wd, () => currentPos)
case _ =>
renderNormal(config, interp, jsonnetCode, path, wd, () => currentPos, stdout)
}
case _ => renderNormal(config, interp, jsonnetCode, path, wd, () => currentPos)
case _ => renderNormal(config, interp, jsonnetCode, path, wd, () => currentPos, stdout)
}

if (profilerInstance != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package sjsonnet

import java.io.OutputStream
import java.nio.charset.Charset
import java.util.Arrays

/**
* An unsynchronized byte array output stream for single-threaded CLI rendering.
*
* Drop-in replacement for [[java.io.ByteArrayOutputStream]] that eliminates all synchronization
* overhead. In sjsonnet's stdout output path, this reduces ~thousands of `synchronized` write calls
* to a single bulk [[writeTo]] at the end.
*
* This class is NOT thread-safe. It is designed for the single-threaded Jsonnet evaluation pipeline
* where output is buffered in memory, then flushed once to stdout or a file.
*
* Inspired by Apache Pekko's `UnsynchronizedByteArrayInputStream` and Apache Commons IO's
* unsynchronized stream classes.
*/
// @NotThreadSafe
final class UnsynchronizedByteArrayOutputStream(initialCapacity: Int) extends OutputStream {
require(initialCapacity >= 0, s"Negative initial capacity: $initialCapacity")

private var buf: Array[Byte] = new Array[Byte](initialCapacity)
private var count: Int = 0

def this() = this(32)

/** Ensure the buffer can hold at least `minCapacity` bytes, doubling growth to amortize cost. */
@inline private def ensureCapacity(minCapacity: Int): Unit = {
if (minCapacity > buf.length) {
var newCapacity = math.max(buf.length << 1, minCapacity)
// Guard against overflow
if (newCapacity < 0) newCapacity = Int.MaxValue
buf = Arrays.copyOf(buf, newCapacity)
}
}

override def write(b: Int): Unit = {
ensureCapacity(count + 1)
buf(count) = b.toByte
count += 1
}

override def write(b: Array[Byte], off: Int, len: Int): Unit = {
if ((off | len | (b.length - off - len)) < 0) throw new IndexOutOfBoundsException
if (len == 0) return
ensureCapacity(count + len)
System.arraycopy(b, off, buf, count, len)
count += len
}

/**
* Writes the complete buffered contents to the specified output stream in a single call. This
* results in exactly one synchronized write when the target is a [[java.io.PrintStream]].
*/
def writeTo(out: OutputStream): Unit = out.write(buf, 0, count)

/** Returns the current number of bytes written to this stream. */
def size: Int = count

/** Resets the buffer so that the stream can be reused. The underlying buffer is not freed. */
def reset(): Unit = count = 0

/** Returns a copy of the buffered data as a byte array. */
def toByteArray: Array[Byte] = Arrays.copyOf(buf, count)

/** Converts the buffered data to a string using the specified charset. */
def toString(charset: Charset): String = new String(buf, 0, count, charset)

override def toString: String = new String(buf, 0, count)

/** Closing this stream has no effect. The buffer remains valid. */
override def close(): Unit = () // no-op
}