Skip to content

Commit 07dc5be

Browse files
Add --flamegraph flag for folded stack profile output (#640)
## Summary - Adds a `--flamegraph <file>` CLI flag that writes a flame graph profile in Brendan Gregg's folded stack format - Introduces `FlameGraphProfiler` that hooks into the existing `Evaluator` stack depth tracking, pushing/popping frame names at every `Apply`, `ApplyBuiltin`, import, comprehension, and object field evaluation - Profile output can be fed directly into [FlameGraph](https://github.com/brendangregg/FlameGraph) tools: `flamegraph.pl profile.txt > profile.svg` ## Test plan - [x] `sjsonnet.jvm[3.3.7].test` passes (140/140) - [x] `sjsonnet.jvm[2.13.18].compile` passes - [ ] Manual testing: `sjsonnet --flamegraph /tmp/profile.txt <input>.jsonnet` produces valid folded stack output
1 parent b351e0e commit 07dc5be

5 files changed

Lines changed: 114 additions & 24 deletions

File tree

sjsonnet/src-jvm-native/sjsonnet/Config.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ final case class Config(
167167
doc = "Number of allowed stack frames (default 500)"
168168
)
169169
maxStack: Int = 500,
170+
@arg(
171+
name = "flamegraph",
172+
doc =
173+
"Write a flame graph profile in folded stack format to the given file. Use with https://github.com/brendangregg/FlameGraph"
174+
)
175+
flamegraph: Option[String] = None,
170176
@arg(
171177
doc = "The jsonnet file you wish to evaluate",
172178
positional = true

sjsonnet/src-jvm-native/sjsonnet/SjsonnetMainBase.scala

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,8 @@ object SjsonnetMainBase {
169169
},
170170
warn,
171171
std,
172-
debugStats = debugStats
172+
debugStats = debugStats,
173+
flamegraphFile = config.flamegraph
173174
)
174175
res <- {
175176
if (hasWarnings && config.fatalWarnings.value) Left("")
@@ -318,7 +319,8 @@ object SjsonnetMainBase {
318319
warnLogger: Evaluator.Logger,
319320
std: Val.Obj,
320321
evaluatorOverride: Option[Evaluator] = None,
321-
debugStats: DebugStats = null): Either[String, String] = {
322+
debugStats: DebugStats = null,
323+
flamegraphFile: Option[String] = None): Either[String, String] = {
322324

323325
val (jsonnetCode, path) =
324326
if (config.exec.value) (file, wd / Util.wrapInLessThanGreaterThan("exec"))
@@ -348,6 +350,7 @@ object SjsonnetMainBase {
348350
)
349351

350352
var currentPos: Position = null
353+
var profiler: FlameGraphProfiler = null
351354
val interp = new Interpreter(
352355
queryExtVar = (key: String) => extBinding.get(key).map(ExternalVariable.code),
353356
queryTlaVar = (key: String) => tlaBinding.get(key).map(ExternalVariable.code),
@@ -365,13 +368,19 @@ object SjsonnetMainBase {
365368
resolver: CachedResolver,
366369
extVars: String => Option[Expr],
367370
wd: Path,
368-
settings: Settings): Evaluator =
369-
evaluatorOverride.getOrElse(
371+
settings: Settings): Evaluator = {
372+
val ev = evaluatorOverride.getOrElse(
370373
super.createEvaluator(resolver, extVars, wd, settings)
371374
)
375+
if (flamegraphFile.isDefined) {
376+
profiler = new FlameGraphProfiler
377+
ev.flameGraphProfiler = profiler
378+
}
379+
ev
380+
}
372381
}
373382

374-
(config.multi, config.yamlStream.value) match {
383+
val result = (config.multi, config.yamlStream.value) match {
375384
case (Some(multiPath), _) =>
376385
val trailingNewline = !config.noTrailingNewline.value
377386
interp.interpret(jsonnetCode, OsPath(path)).flatMap {
@@ -437,8 +446,12 @@ object SjsonnetMainBase {
437446
case _ => renderNormal(config, interp, jsonnetCode, path, wd, () => currentPos)
438447
}
439448
case _ => renderNormal(config, interp, jsonnetCode, path, wd, () => currentPos)
440-
441449
}
450+
451+
if (profiler != null)
452+
flamegraphFile.foreach(profiler.writeTo)
453+
454+
result
442455
}
443456

444457
/**

sjsonnet/src/sjsonnet/Evaluator.scala

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class Evaluator(
3030

3131
private[this] var stackDepth: Int = 0
3232
private[this] val maxStack: Int = settings.maxStack
33+
private[sjsonnet] var flameGraphProfiler: FlameGraphProfiler = _
3334

3435
@inline private[sjsonnet] final def checkStackDepth(pos: Position): Unit = {
3536
stackDepth += 1
@@ -39,8 +40,24 @@ class Evaluator(
3940
Error.fail("Max stack frames exceeded.", pos)
4041
}
4142

42-
@inline private[sjsonnet] final def decrementStackDepth(): Unit =
43+
@inline private[sjsonnet] final def checkStackDepth(pos: Position, expr: Expr): Unit = {
44+
stackDepth += 1
45+
if (flameGraphProfiler != null) flameGraphProfiler.push(expr.exprErrorString)
46+
if (stackDepth > maxStack)
47+
Error.fail("Max stack frames exceeded.", pos)
48+
}
49+
50+
@inline private[sjsonnet] final def checkStackDepth(pos: Position, name: String): Unit = {
51+
stackDepth += 1
52+
if (flameGraphProfiler != null) flameGraphProfiler.push(name)
53+
if (stackDepth > maxStack)
54+
Error.fail("Max stack frames exceeded.", pos)
55+
}
56+
57+
@inline private[sjsonnet] final def decrementStackDepth(): Unit = {
4358
stackDepth -= 1
59+
if (flameGraphProfiler != null) flameGraphProfiler.pop()
60+
}
4461

4562
def materialize(v: Val): Value = Materializer.apply(v)
4663
val cachedImports: collection.mutable.HashMap[Path, Val] =
@@ -230,7 +247,7 @@ class Evaluator(
230247
*/
231248
protected def visitApply(e: Apply)(implicit scope: ValScope): Val = {
232249
if (debugStats != null) debugStats.functionCalls += 1
233-
checkStackDepth(e.pos)
250+
checkStackDepth(e.pos, e)
234251
try {
235252
val lhs = visitExpr(e.value)
236253
implicit val tailstrictMode: TailstrictMode =
@@ -246,7 +263,7 @@ class Evaluator(
246263

247264
protected def visitApply0(e: Apply0)(implicit scope: ValScope): Val = {
248265
if (debugStats != null) debugStats.functionCalls += 1
249-
checkStackDepth(e.pos)
266+
checkStackDepth(e.pos, e)
250267
try {
251268
val lhs = visitExpr(e.value)
252269
implicit val tailstrictMode: TailstrictMode =
@@ -261,7 +278,7 @@ class Evaluator(
261278

262279
protected def visitApply1(e: Apply1)(implicit scope: ValScope): Val = {
263280
if (debugStats != null) debugStats.functionCalls += 1
264-
checkStackDepth(e.pos)
281+
checkStackDepth(e.pos, e)
265282
try {
266283
val lhs = visitExpr(e.value)
267284
implicit val tailstrictMode: TailstrictMode =
@@ -277,7 +294,7 @@ class Evaluator(
277294

278295
protected def visitApply2(e: Apply2)(implicit scope: ValScope): Val = {
279296
if (debugStats != null) debugStats.functionCalls += 1
280-
checkStackDepth(e.pos)
297+
checkStackDepth(e.pos, e)
281298
try {
282299
val lhs = visitExpr(e.value)
283300
implicit val tailstrictMode: TailstrictMode =
@@ -295,7 +312,7 @@ class Evaluator(
295312

296313
protected def visitApply3(e: Apply3)(implicit scope: ValScope): Val = {
297314
if (debugStats != null) debugStats.functionCalls += 1
298-
checkStackDepth(e.pos)
315+
checkStackDepth(e.pos, e)
299316
try {
300317
val lhs = visitExpr(e.value)
301318
implicit val tailstrictMode: TailstrictMode =
@@ -316,7 +333,7 @@ class Evaluator(
316333

317334
protected def visitApplyBuiltin0(e: ApplyBuiltin0): Val = {
318335
if (debugStats != null) debugStats.builtinCalls += 1
319-
checkStackDepth(e.pos)
336+
checkStackDepth(e.pos, e)
320337
try {
321338
val result = e.func.evalRhs(this, e.pos)
322339
if (e.tailstrict) TailCall.resolve(result) else result
@@ -325,7 +342,7 @@ class Evaluator(
325342

326343
protected def visitApplyBuiltin1(e: ApplyBuiltin1)(implicit scope: ValScope): Val = {
327344
if (debugStats != null) debugStats.builtinCalls += 1
328-
checkStackDepth(e.pos)
345+
checkStackDepth(e.pos, e)
329346
try {
330347
if (e.tailstrict) {
331348
TailCall.resolve(e.func.evalRhs(visitExpr(e.a1), this, e.pos))
@@ -337,7 +354,7 @@ class Evaluator(
337354

338355
protected def visitApplyBuiltin2(e: ApplyBuiltin2)(implicit scope: ValScope): Val = {
339356
if (debugStats != null) debugStats.builtinCalls += 1
340-
checkStackDepth(e.pos)
357+
checkStackDepth(e.pos, e)
341358
try {
342359
if (e.tailstrict) {
343360
TailCall.resolve(e.func.evalRhs(visitExpr(e.a1), visitExpr(e.a2), this, e.pos))
@@ -349,7 +366,7 @@ class Evaluator(
349366

350367
protected def visitApplyBuiltin3(e: ApplyBuiltin3)(implicit scope: ValScope): Val = {
351368
if (debugStats != null) debugStats.builtinCalls += 1
352-
checkStackDepth(e.pos)
369+
checkStackDepth(e.pos, e)
353370
try {
354371
if (e.tailstrict) {
355372
TailCall.resolve(
@@ -363,7 +380,7 @@ class Evaluator(
363380

364381
protected def visitApplyBuiltin4(e: ApplyBuiltin4)(implicit scope: ValScope): Val = {
365382
if (debugStats != null) debugStats.builtinCalls += 1
366-
checkStackDepth(e.pos)
383+
checkStackDepth(e.pos, e)
367384
try {
368385
if (e.tailstrict) {
369386
TailCall.resolve(
@@ -391,7 +408,7 @@ class Evaluator(
391408

392409
protected def visitApplyBuiltin(e: ApplyBuiltin)(implicit scope: ValScope): Val = {
393410
if (debugStats != null) debugStats.builtinCalls += 1
394-
checkStackDepth(e.pos)
411+
checkStackDepth(e.pos, e)
395412
try {
396413
val arr = new Array[Eval](e.argExprs.length)
397414
var idx = 0
@@ -504,7 +521,7 @@ class Evaluator(
504521
cachedImports.getOrElseUpdate(
505522
p, {
506523
if (debugStats != null) debugStats.importCalls += 1
507-
checkStackDepth(e.pos)
524+
checkStackDepth(e.pos, e)
508525
try {
509526
val doc = resolver.parse(p, str) match {
510527
case Right((expr, _)) => expr
@@ -732,7 +749,7 @@ class Evaluator(
732749
def evalRhs(vs: ValScope, es: EvalScope, fs: FileScope, pos: Position): Val =
733750
visitExprWithTailCallSupport(rhs)(vs)
734751
override def evalDefault(expr: Expr, vs: ValScope, es: EvalScope): Val = {
735-
checkStackDepth(expr.pos)
752+
checkStackDepth(expr.pos, "default")
736753
try visitExpr(expr)(vs)
737754
finally decrementStackDepth()
738755
}
@@ -923,9 +940,10 @@ class Evaluator(
923940
case Member.Field(offset, fieldName, plus, null, sep, rhs) =>
924941
val k = visitFieldName(fieldName, offset)
925942
if (k != null) {
943+
val fieldKey = k
926944
val v = new Val.Obj.Member(plus, sep) {
927945
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
928-
checkStackDepth(rhs.pos)
946+
checkStackDepth(rhs.pos, fieldKey)
929947
try visitExpr(rhs)(makeNewScope(self, sup))
930948
finally decrementStackDepth()
931949
}
@@ -938,9 +956,10 @@ class Evaluator(
938956
case Member.Field(offset, fieldName, false, argSpec, sep, rhs) =>
939957
val k = visitFieldName(fieldName, offset)
940958
if (k != null) {
959+
val fieldKey = k
941960
val v = new Val.Obj.Member(false, sep) {
942961
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
943-
checkStackDepth(rhs.pos)
962+
checkStackDepth(rhs.pos, fieldKey)
944963
try visitMethod(rhs, argSpec, offset)(makeNewScope(self, sup))
945964
finally decrementStackDepth()
946965
}
@@ -982,7 +1001,7 @@ class Evaluator(
9821001
k,
9831002
new Val.Obj.Member(e.plus, Visibility.Normal, deprecatedSkipAsserts = true) {
9841003
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
985-
checkStackDepth(e.value.pos)
1004+
checkStackDepth(e.value.pos, "object comprehension")
9861005
try {
9871006
lazy val newScope: ValScope = s.extend(newBindings, self, sup)
9881007
lazy val newBindings = visitBindings(binds, newScope)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package sjsonnet
2+
3+
import java.io.{BufferedWriter, FileWriter}
4+
5+
/**
6+
* Collects stack samples during Jsonnet evaluation and writes them in Brendan Gregg's folded stack
7+
* format, suitable for generating flame graphs with https://github.com/brendangregg/FlameGraph.
8+
*
9+
* Each call to [[push]] records a new frame on the current stack. Each call to [[pop]] removes the
10+
* top frame. A sample (incrementing the count for the current stack) is taken on every [[push]], so
11+
* deeper call trees contribute proportionally more samples.
12+
*/
13+
final class FlameGraphProfiler {
14+
private val stack = new java.util.ArrayDeque[String]()
15+
private val counts = new java.util.HashMap[String, java.lang.Long]()
16+
17+
def push(name: String): Unit = {
18+
stack.push(name)
19+
val key = foldedStack()
20+
val prev = counts.get(key)
21+
counts.put(key, if (prev == null) 1L else prev + 1L)
22+
}
23+
24+
def pop(): Unit =
25+
if (!stack.isEmpty) stack.pop()
26+
27+
private def foldedStack(): String = {
28+
val sb = new StringBuilder
29+
val it = stack.descendingIterator()
30+
var first = true
31+
while (it.hasNext) {
32+
if (!first) sb.append(';')
33+
sb.append(it.next())
34+
first = false
35+
}
36+
sb.toString
37+
}
38+
39+
def writeTo(path: String): Unit = {
40+
val w = new BufferedWriter(new FileWriter(path))
41+
try {
42+
val it = counts.entrySet().iterator()
43+
while (it.hasNext) {
44+
val e = it.next()
45+
w.write(e.getKey)
46+
w.write(' ')
47+
w.write(e.getValue.toString)
48+
w.newLine()
49+
}
50+
} finally w.close()
51+
}
52+
}

sjsonnet/src/sjsonnet/Interpreter.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ class Interpreter(
251251
f.evalRhs(vs, es, fs, pos)
252252

253253
override def evalDefault(expr: Expr, vs: ValScope, es: EvalScope): Val = {
254-
evaluator.checkStackDepth(expr.pos)
254+
evaluator.checkStackDepth(expr.pos, "default")
255255
try
256256
evaluator.visitExpr(expr)(
257257
if (tlaExpressions.exists(_ eq expr)) ValScope.empty else vs

0 commit comments

Comments
 (0)