Skip to content

Commit d3b9486

Browse files
committed
chore: profiler and flamegraph support
1 parent d974cf1 commit d3b9486

File tree

14 files changed

+275
-118
lines changed

14 files changed

+275
-118
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,11 @@ jobs:
4646
fail-fast: true
4747
max-parallel: 3
4848
matrix:
49-
os: [ ubuntu-latest, macos-latest, windows-latest ]
49+
os: [ ubuntu-latest, macos-latest ]
5050
jdk: [ ea ]
5151
include:
5252
- os: macos-latest
5353
native_task: :macosArm64Test :macosX64Test
54-
- os: windows-latest
55-
native_task: :mingwX64Test
5654
- os: ubuntu-latest
5755
native_task: :linuxX64Test
5856

backend/jvm/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ dependencies {
8686
implementation(libs.ktor.server.cors)
8787
implementation(libs.ktor.server.host.common)
8888
implementation(libs.ktor.server.auto.head)
89+
implementation(libs.ktor.server.partial.content)
8990
implementation(libs.ktor.server.resources)
9091
implementation(libs.ktor.server.auth)
9192
implementation(libs.ktor.server.auth.jwt)
@@ -109,13 +110,16 @@ dependencies {
109110
implementation(libs.ktor.cohort.core)
110111
implementation(libs.ktor.cohort.hikari)
111112
implementation(libs.micrometer.prometheus)
113+
implementation(libs.ap.loader.all)
112114
// Logging
113115
implementation(libs.logback.classic)
116+
implementation("io.ktor:ktor-server-partial-content-jvm:2.3.6")
114117
// Testing
115118
testImplementation(platform(libs.testcontainers.bom))
116119
testImplementation(libs.ktor.server.tests)
117120
testImplementation(libs.testcontainers.junit5)
118121
testImplementation(libs.testcontainers.postgresql)
122+
testImplementation(libs.konsist)
119123

120124
// Copy web app browserDist
121125
webapp(project(path = ":${projects.web.name}", configuration = webapp.name))
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package one.converter;
2+
/*
3+
* Copyright 2022 Andrei Pangin
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import java.lang.reflect.Field;
19+
import java.lang.reflect.Modifier;
20+
import java.util.Calendar;
21+
import java.util.GregorianCalendar;
22+
import java.util.StringTokenizer;
23+
import java.util.regex.Pattern;
24+
25+
public class Arguments {
26+
String title = "Flame Graph";
27+
String highlight;
28+
Pattern include;
29+
Pattern exclude;
30+
double minwidth;
31+
int skip;
32+
boolean reverse;
33+
boolean cpu;
34+
boolean alloc;
35+
boolean live;
36+
boolean lock;
37+
boolean threads;
38+
boolean total;
39+
boolean lines;
40+
boolean bci;
41+
boolean simple;
42+
boolean dot;
43+
boolean collapsed;
44+
long from;
45+
long to;
46+
String input;
47+
String output;
48+
49+
public Arguments(String... args) {
50+
for (int i = 0; i < args.length; i++) {
51+
String arg = args[i];
52+
if (arg.startsWith("--")) {
53+
try {
54+
Field f = Arguments.class.getDeclaredField(arg.substring(2));
55+
if ((f.getModifiers() & (Modifier.PRIVATE | Modifier.STATIC | Modifier.FINAL)) != 0) {
56+
throw new IllegalStateException(arg);
57+
}
58+
59+
Class<?> type = f.getType();
60+
if (type == String.class) {
61+
f.set(this, args[++i]);
62+
} else if (type == boolean.class) {
63+
f.setBoolean(this, true);
64+
} else if (type == int.class) {
65+
f.setInt(this, Integer.parseInt(args[++i]));
66+
} else if (type == double.class) {
67+
f.setDouble(this, Double.parseDouble(args[++i]));
68+
} else if (type == long.class) {
69+
f.setLong(this, parseTimestamp(args[++i]));
70+
} else if (type == Pattern.class) {
71+
f.set(this, Pattern.compile(args[++i]));
72+
}
73+
} catch (NoSuchFieldException | IllegalAccessException e) {
74+
throw new IllegalArgumentException(arg);
75+
}
76+
} else if (!arg.isEmpty()) {
77+
if (input == null) {
78+
input = arg;
79+
} else {
80+
output = arg;
81+
}
82+
}
83+
}
84+
}
85+
86+
// Milliseconds or HH:mm:ss.S or yyyy-MM-dd'T'HH:mm:ss.S
87+
private long parseTimestamp(String time) {
88+
if (time.indexOf(':') < 0) {
89+
return Long.parseLong(time);
90+
}
91+
92+
GregorianCalendar cal = new GregorianCalendar();
93+
StringTokenizer st = new StringTokenizer(time, "-:.T");
94+
95+
if (time.indexOf('T') > 0) {
96+
cal.set(Calendar.YEAR, Integer.parseInt(st.nextToken()));
97+
cal.set(Calendar.MONTH, Integer.parseInt(st.nextToken()) - 1);
98+
cal.set(Calendar.DAY_OF_MONTH, Integer.parseInt(st.nextToken()));
99+
}
100+
cal.set(Calendar.HOUR_OF_DAY, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0);
101+
cal.set(Calendar.MINUTE, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0);
102+
cal.set(Calendar.SECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0);
103+
cal.set(Calendar.MILLISECOND, st.hasMoreTokens() ? Integer.parseInt(st.nextToken()) : 0);
104+
105+
return cal.getTimeInMillis();
106+
}
107+
}

backend/jvm/src/main/kotlin/dev/suresh/lang/FFM.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
package dev.suresh.lang
22

3-
import dev.suresh.LINKER
4-
import dev.suresh.SYMBOL_LOOKUP
5-
import dev.suresh.downcallHandle
6-
import dev.suresh.findOrNull
3+
import dev.suresh.*
74
import io.github.oshai.kotlinlogging.KLogger
85
import java.lang.foreign.*
96
import java.lang.invoke.MethodHandle
@@ -14,7 +11,7 @@ import java.time.Instant
1411
object FFM {
1512

1613
context(KLogger)
17-
fun memoryLayout() {
14+
suspend fun memoryLayout() = runOnVirtualThread {
1815
memoryAPIs()
1916
currTime()
2017
strlen("Hello Panama!")

backend/jvm/src/main/kotlin/dev/suresh/lang/VThread.kt

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package dev.suresh.lang
22

33
import dev.suresh.*
44
import io.github.oshai.kotlinlogging.KLogger
5-
import java.lang.foreign.FunctionDescriptor
6-
import java.lang.foreign.ValueLayout
75
import java.util.concurrent.StructuredTaskScope
86
import kotlin.time.Duration.Companion.seconds
97
import kotlinx.datetime.Clock
@@ -16,7 +14,7 @@ import stdlibFeatures
1614
object VThread {
1715

1816
context(KLogger)
19-
fun virtualThreads() {
17+
suspend fun virtualThreads() = runOnVirtualThread {
2018
info { (Greeting().greeting()) }
2119
listOf("main", "jvm", "js").forEach {
2220
info { "common-$it --> ${ClassLoader.getSystemResource("common-$it-res.txt")?.readText()}" }
@@ -29,16 +27,13 @@ object VThread {
2927
structuredConcurrency()
3028
langFeatures()
3129
stdlibFeatures()
32-
33-
getPid()
3430
kotlinxMetaData()
3531
classFileApi()
3632
}
3733
}
3834

3935
context(KLogger)
4036
fun structuredConcurrency() {
41-
4237
info { "Structured concurrency..." }
4338
val taskList =
4439
StructuredTaskScope<String>().use { sts ->
@@ -52,13 +47,14 @@ fun structuredConcurrency() {
5247
else -> {
5348
while (!Thread.currentThread().isInterrupted) {
5449
debug { "Task $it ..." }
50+
Thread.sleep(100)
5551
}
5652
"Task $it"
5753
}
5854
}
5955
}
6056
}
61-
runCatching { sts.joinUntil(start.plus(2.seconds).toJavaInstant()) }
57+
runCatching { sts.joinUntil(start.plus(1.seconds).toJavaInstant()) }
6258
tasks
6359
}
6460

@@ -76,16 +72,6 @@ fun structuredConcurrency() {
7672
}
7773
}
7874

79-
context(KLogger)
80-
fun getPid() {
81-
val getpidAddr = SYMBOL_LOOKUP.findOrNull("getpid")
82-
val getpidDesc = FunctionDescriptor.of(ValueLayout.JAVA_INT)
83-
val getpid = LINKER.downcallHandle(getpidAddr, getpidDesc)
84-
val pid = getpid.invokeExact() as Int
85-
assert(pid.toLong() == ProcessHandle.current().pid())
86-
info { "getpid() = $pid" }
87-
}
88-
8975
context(KLogger)
9076
fun kotlinxMetaData() {
9177
val metadataAnnotation = LocalDateTime::class.java.getAnnotation(Metadata::class.java)

backend/jvm/src/main/kotlin/dev/suresh/plugins/Http.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ import io.ktor.server.plugins.compression.*
1010
import io.ktor.server.plugins.cors.routing.*
1111
import io.ktor.server.plugins.defaultheaders.*
1212
import io.ktor.server.plugins.forwardedheaders.*
13+
import io.ktor.server.plugins.partialcontent.*
1314
import io.ktor.server.request.*
1415
import io.ktor.server.routing.*
1516
import org.slf4j.event.Level
1617

1718
fun Application.configureHTTP() {
1819
install(IgnoreTrailingSlash)
1920

21+
install(PartialContent)
22+
2023
install(AutoHeadResponse)
2124

2225
install(ForwardedHeaders)

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,40 @@ package dev.suresh.routes
33
import dev.suresh.lang.FFM
44
import dev.suresh.lang.VThread
55
import dev.suresh.log.LoggerDelegate
6+
import dev.suresh.runOnVirtualThread
67
import io.github.oshai.kotlinlogging.KotlinLogging
8+
import io.ktor.http.*
79
import io.ktor.server.application.*
810
import io.ktor.server.response.*
911
import io.ktor.server.routing.*
12+
import java.io.PrintStream
13+
import jdk.jfr.Configuration
14+
import jdk.jfr.consumer.RecordingStream
15+
import kotlin.io.path.deleteIfExists
16+
import kotlin.io.path.name
17+
import kotlin.io.path.pathString
18+
import kotlin.time.Duration.Companion.milliseconds
19+
import kotlin.time.toJavaDuration
20+
import kotlinx.coroutines.sync.Mutex
21+
import kotlinx.coroutines.sync.withLock
22+
import one.converter.Arguments
23+
import one.converter.FlameGraph
24+
import one.converter.jfr2flame
25+
import one.jfr.JfrReader
26+
import one.profiler.AsyncProfiler
27+
import one.profiler.AsyncProfilerLoader
28+
import one.profiler.Events
1029

1130
private val logger = KotlinLogging.logger {}
1231

32+
val mutex = Mutex()
33+
34+
val profiler: AsyncProfiler? by lazy {
35+
val ap = AsyncProfilerLoader.loadOrNull()
36+
ap.start(Events.CPU, 1000)
37+
ap
38+
}
39+
1340
fun Route.jvmFeatures() {
1441
get("/ffm") {
1542
call.respondTextWriter { with(LoggerDelegate(this, logger)) { FFM.memoryLayout() } }
@@ -18,4 +45,47 @@ fun Route.jvmFeatures() {
1845
get("/vthreads") {
1946
call.respondTextWriter { with(LoggerDelegate(this, logger)) { VThread.virtualThreads() } }
2047
}
48+
49+
get("/profile") {
50+
// Run the blocking operation on virtual thread and make sure
51+
// only one profile operation is running at a time.
52+
when {
53+
mutex.isLocked -> call.respondText("Profile operation is already running")
54+
else ->
55+
mutex.withLock {
56+
runOnVirtualThread {
57+
val jfrPath = kotlin.io.path.createTempFile("profile", ".jfr")
58+
RecordingStream(Configuration.getConfiguration("profile")).use {
59+
it.enable("jdk.CPULoad").withPeriod(100.milliseconds.toJavaDuration())
60+
it.enable("jdk.JavaMonitorEnter").withStackTrace()
61+
it.startAsync()
62+
Thread.sleep(5_000)
63+
it.dump(jfrPath)
64+
println("JFR file written to ${jfrPath.toAbsolutePath()}")
65+
}
66+
67+
when (call.request.queryParameters.contains("download")) {
68+
true -> {
69+
call.response.header(
70+
HttpHeaders.ContentDisposition,
71+
ContentDisposition.Attachment.withParameter(
72+
ContentDisposition.Parameters.FileName, jfrPath.fileName.name)
73+
.toString())
74+
call.respondFile(jfrPath.toFile())
75+
}
76+
else -> {
77+
val jfr2flame = jfr2flame(JfrReader(jfrPath.pathString), Arguments())
78+
val flameGraph = FlameGraph()
79+
jfr2flame.convert(flameGraph)
80+
81+
call.respondOutputStream(contentType = ContentType.Text.Html) {
82+
flameGraph.dump(PrintStream(this))
83+
}
84+
jfrPath.deleteIfExists()
85+
}
86+
}
87+
}
88+
}
89+
}
90+
}
2191
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package dev.suresh
2+
3+
import com.lemonappdev.konsist.api.Konsist
4+
import com.lemonappdev.konsist.api.ext.list.enumConstants
5+
import com.lemonappdev.konsist.api.ext.list.modifierprovider.withEnumModifier
6+
import com.lemonappdev.konsist.api.ext.list.withAnnotationOf
7+
import com.lemonappdev.konsist.api.verify.assertTrue
8+
import kotlin.test.Test
9+
import kotlinx.serialization.SerialName
10+
import kotlinx.serialization.Serializable
11+
12+
class AppTests {
13+
14+
@Test
15+
fun `make sure the enum classes have serial annotation`() {
16+
Konsist.scopeFromSourceSet("main")
17+
.classes()
18+
.withEnumModifier()
19+
.withAnnotationOf(Serializable::class)
20+
.enumConstants
21+
.assertTrue { it.hasAnnotationOf(SerialName::class) }
22+
}
23+
}

0 commit comments

Comments
 (0)