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
81 changes: 81 additions & 0 deletions bench/src/sjsonnet/bench/RawImportBenchmark.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package sjsonnet.bench

import org.openjdk.jmh.annotations.*
import org.openjdk.jmh.infra.Blackhole
import sjsonnet.*

import java.util.concurrent.TimeUnit

@BenchmarkMode(Array(Mode.AverageTime))
@Fork(1)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
class RawImportBenchmark {
private var root: os.Path = _
private var importBinProgram: String = _
private var importStrProgram: String = _
private var repeatedSmallImportStrProgram: String = _
private var singleImportBinProgram: String = _
private var singleImportStrProgram: String = _

@Setup(Level.Trial)
def setup(): Unit = {
root = os.temp.dir(prefix = "sjsonnet-raw-import-benchmark-")
os.write.over(root / "payload.bin", Array.tabulate[Byte](256 * 1024)(_.toByte))
os.write.over(root / "payload.txt", "x" * (1024 * 1024 + 1))
os.write.over(root / "small.txt", "small payload")
importBinProgram = """std.sum(std.makeArray(
| 300,
| function(_) std.length(importbin "payload.bin")
|))""".stripMargin
importStrProgram = """std.sum(std.makeArray(
| 200,
| function(_) std.length(importstr "payload.txt")
|))""".stripMargin
repeatedSmallImportStrProgram = """std.sum(std.makeArray(
| 300,
| function(_) std.length(importstr "small.txt")
|))""".stripMargin
singleImportBinProgram = """std.length(importbin "payload.bin")"""
singleImportStrProgram = """std.length(importstr "payload.txt")"""
}

@TearDown(Level.Trial)
def teardown(): Unit = os.remove.all(root)

private def evaluate(program: String): ujson.Value = {
val interpreter = new Interpreter(
Map.empty,
Map.empty,
OsPath(root),
new SjsonnetMainBase.SimpleImporter(Seq.empty),
parseCache = new DefaultParseCache
)
interpreter.interpret(program, OsPath(root / "main.jsonnet")) match {
case Right(value) => value
case Left(error) => throw new RuntimeException(error)
}
}

@Benchmark
def repeatedImportBin(blackhole: Blackhole): Unit =
blackhole.consume(evaluate(importBinProgram))

@Benchmark
def repeatedImportStr(blackhole: Blackhole): Unit =
blackhole.consume(evaluate(importStrProgram))

@Benchmark
def repeatedSmallImportStr(blackhole: Blackhole): Unit =
blackhole.consume(evaluate(repeatedSmallImportStrProgram))

@Benchmark
def singleImportBin(blackhole: Blackhole): Unit =
blackhole.consume(evaluate(singleImportBinProgram))

@Benchmark
def singleImportStr(blackhole: Blackhole): Unit =
blackhole.consume(evaluate(singleImportStrProgram))
}
23 changes: 9 additions & 14 deletions sjsonnet/src/sjsonnet/Evaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ class Evaluator(
def materialize(v: Val): Value = Materializer.apply(v)
val cachedImports: collection.mutable.HashMap[Path, Val] =
collection.mutable.HashMap.empty[Path, Val]
private val cachedImportStrings = collection.mutable.HashMap.empty[Path, Val.Str]
private val cachedImportBinaries = collection.mutable.HashMap.empty[Path, Val.Arr]

// Hot path: top 7 types cover 96.1% of all visitExpr calls across benchmarks.
// Order matches the empirically-measured frequency for C1 monomorphic dispatch parity.
Expand Down Expand Up @@ -1266,23 +1268,16 @@ class Evaluator(
}

def visitImportStr(e: ImportStr): Val.Str = {
Val.Str(
e.pos,
importer.resolveAndReadOrFail(e.value, e.pos, binaryData = false)._2.readString()
)
val (path, file) = importer.resolveAndReadOrFail(e.value, e.pos, binaryData = false)
cachedImportStrings.getOrElseUpdate(path, Val.Str(e.pos, file.readString()))
}

def visitImportBin(e: ImportBin): Val.Arr = {
val rawBytes =
importer.resolveAndReadOrFail(e.value, e.pos, binaryData = true)._2.readRawBytes()
val len = rawBytes.length
val arr = new Array[Eval](len)
var i = 0
while (i < len) {
arr(i) = Val.cachedNum(e.pos, (rawBytes(i) & 0xff).toDouble)
i += 1
}
Val.Arr(e.pos, arr)
val (path, file) = importer.resolveAndReadOrFail(e.value, e.pos, binaryData = true)
cachedImportBinaries.getOrElseUpdate(
path,
Val.Arr.fromBytes(e.pos, file.readRawBytes().clone())
)
}

def visitImport(e: Import): Val = {
Expand Down
6 changes: 5 additions & 1 deletion sjsonnet/src/sjsonnet/Val.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,10 @@ object Val {
// After materialization arr becomes non-null; delegate to parent Arr logic.
@inline private def isMaterialized: Boolean = arr ne null

/** Raw byte backing data for zero-copy extraction (e.g. base64 encode). Always non-null. */
/**
* Raw byte backing data for zero-copy extraction (e.g. base64 encode). Always non-null.
* CONTRACT: callers MUST NOT mutate the returned array — it may be shared via import cache.
*/
override def rawBytes: Array[Byte] = byteData

override def value(i: Int): Val = {
Expand Down Expand Up @@ -1624,6 +1627,7 @@ object Val {
}
}

/** CONTRACT: callers MUST NOT mutate the returned array. */
override def rawBytes: Array[Byte] = {
val current = byteData
if (current != null) current
Expand Down
73 changes: 73 additions & 0 deletions sjsonnet/test/src/sjsonnet/EvaluatorRawImportCacheTests.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package sjsonnet

import utest._

object EvaluatorRawImportCacheTests extends TestSuite {
def tests: Tests = Tests {
test("importstr and importbin values are cached independently by path") {
var stringReads = 0
var binaryReads = 0
val binaryContent = Array[Byte](1)

val importer = new Importer {
def resolve(docBase: Path, importName: String): Option[Path] =
Some(DummyPath(importName))

def read(path: Path, binaryData: Boolean): Option[ResolvedFile] =
if (binaryData) {
Some(new ResolvedFile {
def getParserInput(): fastparse.ParserInput =
throw new NotImplementedError("not used by importbin")
def readString(): String =
throw new NotImplementedError("not used by importbin")
def contentHash(): String = "binary"
def readRawBytes(): Array[Byte] = {
binaryReads += 1
binaryContent
}
})
} else {
Some(new ResolvedFile {
def getParserInput(): fastparse.ParserInput =
throw new NotImplementedError("not used by importstr")
def readString(): String = {
stringReads += 1
s"text-$stringReads"
}
def contentHash(): String = "string"
def readRawBytes(): Array[Byte] =
throw new NotImplementedError("not used by importstr")
})
}
}

val interpreter = new Interpreter(
Map.empty,
Map.empty,
DummyPath("root"),
importer,
parseCache = new DefaultParseCache
)
val result = interpreter.interpret(
"""[
| importstr "same",
| importstr "same",
| importbin "same",
| importbin "same",
|]""".stripMargin,
DummyPath("root", "main.jsonnet")
)

result ==> Right(ujson.Arr("text-1", "text-1", ujson.Arr(1), ujson.Arr(1)))
stringReads ==> 1
binaryReads ==> 1

binaryContent(0) = 9
interpreter.interpret(
"""importbin "same"""",
DummyPath("root", "second.jsonnet")
) ==> Right(ujson.Arr(1))
binaryReads ==> 1
}
}
}
Loading