Skip to content

Commit 373fa42

Browse files
committed
chore: Async profiler 4.0 flamegraph and heatmap support
1 parent 533c1b9 commit 373fa42

File tree

4 files changed

+54
-34
lines changed

4 files changed

+54
-34
lines changed

backend/jvm/src/main/kotlin/dev/suresh/routes/Mgmt.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,22 +149,28 @@ fun Route.mgmtRoutes() {
149149
}
150150
}
151151

152-
get("/profile") {
152+
get("/profile/{type?}") {
153153
when (mutex.isLocked) {
154154
true -> call.respondText("Profile operation is already running")
155155
else ->
156156
mutex.withLock {
157-
when (call.request.queryParameters.contains("download")) {
158-
true -> {
157+
val type = call.parameters["type"]?.lowercase() ?: "html"
158+
when (type) {
159+
"download" -> {
159160
val jfrPath = Profiling.jfrSnapshot()
160161
call.response.header(
161162
ContentDisposition,
162163
Attachment.withParameter(FileName, jfrPath.fileName.name).toString())
163164
call.respondFile(jfrPath.toFile())
164165
jfrPath.deleteIfExists()
165166
}
166-
else ->
167-
call.respondText(contentType = ContentType.Text.Html) { Profiling.flameGraph() }
167+
168+
"html",
169+
"heatmap" ->
170+
call.respondText(contentType = ContentType.Text.Html) {
171+
Profiling.convertJfr(type)
172+
}
173+
else -> error("Invalid profile type: $type")
168174
}
169175
}
170176
}

backend/profiling/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ kotlin {
1717
dependencies {
1818
implementation(libs.jmc.common)
1919
implementation(libs.jmc.jfr)
20-
implementation(libs.ap.converter)
20+
implementation(libs.ap.jfr.converter)
21+
implementation(libs.bytesize)
2122
// implementation(libs.ap.loader.all)
2223
}
2324
kotlin.srcDir("src/main/kotlin")

backend/profiling/src/main/kotlin/dev/suresh/Profiling.kt

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,25 @@
22

33
package dev.suresh
44

5-
import Arguments
6-
import FlameGraph
5+
import BuildConfig
76
import com.sun.management.HotSpotDiagnosticMXBean
87
import io.github.oshai.kotlinlogging.KotlinLogging
98
import java.io.ByteArrayOutputStream
10-
import java.io.PrintStream
119
import java.lang.management.ManagementFactory
1210
import java.nio.charset.StandardCharsets
1311
import java.nio.file.Path
1412
import jdk.jfr.Configuration
1513
import jdk.jfr.FlightRecorder
1614
import jdk.jfr.consumer.RecordingStream
1715
import jdk.management.VirtualThreadSchedulerMXBean
18-
import jfr2flame
19-
import kotlin.io.path.createTempFile
20-
import kotlin.io.path.deleteIfExists
21-
import kotlin.io.path.pathString
16+
import kotlin.io.path.*
2217
import kotlin.time.Duration
2318
import kotlin.time.Duration.Companion.milliseconds
2419
import kotlin.time.Duration.Companion.minutes
2520
import kotlin.time.Duration.Companion.seconds
2621
import kotlin.time.toJavaDuration
22+
import me.saket.bytesize.binaryBytes
23+
import one.convert.*
2724
import one.jfr.JfrReader
2825

2926
object Profiling {
@@ -85,20 +82,36 @@ object Profiling {
8582
}
8683
}
8784
}
88-
log.info { "JFR file written to ${jfrPath.toAbsolutePath()}" }
85+
log.info {
86+
"JFR file written to ${jfrPath.toAbsolutePath()} (${jfrPath.fileSize().binaryBytes})"
87+
}
8988
jfrPath
9089
}
9190

92-
suspend fun flameGraph(maxAge: Duration = 2.minutes): String = runOnVirtualThread {
93-
val jfrPath = jfrSnapshot(maxAge = maxAge, maxSizeBytes = 100_000_000)
94-
JfrReader(jfrPath.pathString).use {
95-
val jfr2flame = jfr2flame(it, Arguments())
96-
val flameGraph = FlameGraph()
97-
jfr2flame.convert(flameGraph)
98-
val bos = ByteArrayOutputStream()
99-
flameGraph.dump(PrintStream(bos))
100-
jfrPath.deleteIfExists()
101-
bos.toString(StandardCharsets.UTF_8)
102-
}
103-
}
91+
suspend fun convertJfr(format: String = "html", maxAge: Duration = 2.minutes): String =
92+
runOnVirtualThread {
93+
val jfrPath = jfrSnapshot(maxAge, 100_000_000)
94+
val args = Arguments("--output", format, "--reverse", "--title", BuildConfig.name)
95+
96+
val converter =
97+
JfrReader(jfrPath.pathString).use {
98+
when (format) {
99+
"heatmap" -> JfrToHeatmap(it, args)
100+
"html" -> JfrToFlame(it, args)
101+
else -> error("Unsupported format: $format")
102+
}.apply { convert() }
103+
}
104+
105+
val result =
106+
ByteArrayOutputStream().use { out ->
107+
when (converter) {
108+
is JfrToHeatmap -> converter.dump(out)
109+
is JfrToFlame -> converter.dump(out)
110+
else -> error("Unsupported converter for $format")
111+
}
112+
out.toString(StandardCharsets.UTF_8)
113+
}
114+
jfrPath.deleteIfExists()
115+
result
116+
}
104117
}

gradle/libs.versions.toml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ kotlinx-benchmark = "0.4.13"
3939
kotlinx-fuzz = "0.2.2"
4040
kotlinx-metadata = "0.9.0"
4141
kotlinx-reflect-lite = "1.1.0"
42-
kotlin-wrappers = "2025.4.15"
42+
kotlin-wrappers = "2025.4.16"
4343
kotlin-redacted = "1.13.0"
4444
kotlin-serviceloader = "0.0.15"
4545
kotlinx-multik = "0.2.3"
@@ -58,7 +58,7 @@ decoroutinator = "2.4.8"
5858
spring-boot = "3.4.4"
5959
spring-depmgmt = "1.1.7"
6060
ktor = "3.1.2"
61-
ktor-cohort = "2.6.2"
61+
ktor-cohort = "2.7.0"
6262
otel = "1.49.0"
6363
otel-instr = "2.15.0"
6464
otel-instr-alpha = "2.15.0-alpha"
@@ -88,11 +88,11 @@ oshi = "6.8.1"
8888
junit = "5.13.0-M2"
8989
koin = "4.1.0-Beta7"
9090
koin-annotations = "2.0.1-Beta1"
91-
metro = "0.1.2"
91+
metro = "0.2.0"
9292
kotest = "6.0.0.M3"
9393
mockk = "1.14.0"
9494
mokkery = "2.7.2"
95-
wiremock = "3.12.1"
95+
wiremock = "4.0.0-beta.1"
9696
wiremock-kotlin = "2.1.1"
9797
okhttp = "5.0.0-alpha.14"
9898
slf4j = "2.1.0-alpha1"
@@ -135,7 +135,7 @@ ksoup = "0.2.2"
135135
java-keyring = "1.0.4"
136136
java-keychain = "1.1.0"
137137
webjars-xterm = "5.1.0"
138-
arrow-suspendapp = "0.5.0"
138+
arrow-suspendapp = "2.1.0"
139139
exposed = "0.61.0"
140140
postgresql = "42.7.5"
141141
hikariCP = "6.3.0"
@@ -158,8 +158,8 @@ tcp-javanioproxy = "1.6"
158158
kubernetes-client = "23.0.0"
159159
reflect-typeparamresolver = "1.0.3"
160160
reflect-typetools = "0.6.3"
161-
async-profiler = "3.0"
162-
ap-loader-all = "3.0-9"
161+
async-profiler = "4.0"
162+
ap-loader-all = "4.0-10"
163163
openjdk-jmc = "9.1.0"
164164
airlift-aircompressor = "2.0.2"
165165
airlift-security = "332"
@@ -549,7 +549,7 @@ sourceBuddy = { module = "com.javax0.sourcebuddy:Source
549549
maven-mima = { module = "eu.maveniverse.maven.mima:mima" , version.ref = "maven-mima"}
550550
maven-archeologist = { module = "com.squareup.tools.build:maven-archeologist" , version.ref = "maven-archeologist"}
551551

552-
ap-converter = { module = "tools.profiler:async-profiler-converter" , version.ref = "async-profiler" }
552+
ap-jfr-converter = { module = "tools.profiler:jfr-converter" , version.ref = "async-profiler" }
553553
ap-loader-all = { module = "me.bechberger:ap-loader-all" , version.ref = "ap-loader-all"}
554554
jmc-common = { module = "org.openjdk.jmc:common" , version.ref = "openjdk-jmc"}
555555
jmc-jfr = { module = "org.openjdk.jmc:flightrecorder" , version.ref = "openjdk-jmc"}

0 commit comments

Comments
 (0)