Skip to content

Commit f96fba0

Browse files
bioballHT154stackoverflow
authored
Add a CPU profiler (#1823)
This introduces a way to run CPU profiling. The profiler produces output in pprof format. CLI users can enable CPU profiling via command line flags. Profiling is otherwise not offered as an API. Co-authored-by: Jen Basch <jbasch94@gmail.com> Co-authored-by: Islon Scherer <islonscherer@gmail.com>
1 parent d062312 commit f96fba0

48 files changed

Lines changed: 1480 additions & 38 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎THIRD-PARTY-NOTICES.txt‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,3 +408,56 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
408408
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
409409

410410
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
411+
412+
28) protobuf-java
413+
License URL: https://github.com/protocolbuffers/protobuf/blob/main/LICENSE
414+
415+
Copyright 2008 Google Inc. All rights reserved.
416+
417+
Redistribution and use in source and binary forms, with or without
418+
modification, are permitted provided that the following conditions are
419+
met:
420+
421+
* Redistributions of source code must retain the above copyright
422+
notice, this list of conditions and the following disclaimer.
423+
* Redistributions in binary form must reproduce the above
424+
copyright notice, this list of conditions and the following disclaimer
425+
in the documentation and/or other materials provided with the
426+
distribution.
427+
* Neither the name of Google Inc. nor the names of its
428+
contributors may be used to endorse or promote products derived from
429+
this software without specific prior written permission.
430+
431+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
432+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
433+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
434+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
435+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
436+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
437+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
438+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
439+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
440+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
441+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
442+
443+
Code generated by the Protocol Buffer compiler is owned by the owner
444+
of the input file used when generating it. This code is not
445+
standalone and requires a support library to be linked with it. This
446+
support library is itself covered by the above license.
447+
448+
29) pprof
449+
License URL: https://github.com/google/pprof/blob/main/LICENSE
450+
451+
Copyright 2016 Google Inc. All Rights Reserved.
452+
453+
Licensed under the Apache License, Version 2.0 (the "License");
454+
you may not use this file except in compliance with the License.
455+
You may obtain a copy of the License at
456+
457+
http://www.apache.org/licenses/LICENSE-2.0
458+
459+
Unless required by applicable law or agreed to in writing, software
460+
distributed under the License is distributed on an "AS IS" BASIS,
461+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
462+
See the License for the specific language governing permissions and
463+
limitations under the License.

‎build-logic/src/main/kotlin/NativeImageBuild.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ abstract class NativeImageBuild : DefaultTask() {
227227
add("--initialize-at-run-time=org.pkl.core.util.DebugLogger")
228228
// needed for jline-terminal-jni
229229
add("--initialize-at-run-time=org.jline.nativ,org.jline.terminal.impl.jni")
230+
// needed for protobuf-java
231+
add("--initialize-at-run-time=com.google.protobuf.UnsafeUtil")
230232
add("--no-fallback")
231233
add("-H:IncludeResources=org/pkl/core/stdlib/.*\\.pkl")
232234
add("-H:IncludeResources=org/jline/utils/.*")

‎build-logic/src/main/kotlin/pklFatJar.gradle.kts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ val relocations =
6666

6767
// pkl-codegen-kotlin dependencies
6868
"com.squareup.kotlinpoet." to "org.pkl.thirdparty.kotlinpoet.",
69+
70+
// pkl-profiler dependencies
71+
"com.google.protobuf." to "org.pkl.thirdparty.com.google.protobuf.",
72+
"com.google.perftools." to "org.pkl.thirdparty.com.google.perftools.",
73+
"google.protobuf." to "org.pkl.thirdparty.google.protobuf.",
6974
)
7075

7176
for ((key, value) in relocations) {

‎docs/modules/pkl-cli/pages/index.adoc‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,24 @@ When enabled, type constraint failures will show intermediate values in the asse
450450
Use `--no-power-assertions` to disable this feature if you prefer simpler output or better performance.
451451
====
452452

453+
[[profile-cpu-output]]
454+
.--profile-cpu-output
455+
[%collapsible]
456+
====
457+
Default: (none) +
458+
The file to write profiler output to. Omitting this option disables CPU profiling entirely. +
459+
460+
The CPU profiler writes output in https://github.com/google/pprof[pprof] format.
461+
====
462+
463+
[[profile-cpu-sample-period]]
464+
.--profile-cpu-sample-period
465+
[%collapsible]
466+
====
467+
Default: 10 +
468+
The sampling duration for the CPU sampler, in milliseconds.
469+
====
470+
453471
This command also takes <<common-options, common options>>.
454472

455473
[[command-server]]

‎gradle/libs.versions.toml‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ nullaway = "0.13.7"
6262
nullawayPlugin = "3.0.0"
6363
nuValidator = "26.7.3"
6464
paguro = "3.10.3"
65+
protobufPlugin = "0.10.0"
66+
protobuf = "4.35.1"
6567
shadowPlugin = "9.5.1"
6668
slf4j = "2.0.20"
6769
snakeYaml = "3.0.1"
@@ -117,6 +119,8 @@ nullaway = { group = "com.uber.nullaway", name = "nullaway", version.ref = "null
117119
nullawayPlugin = { group = "net.ltgt.gradle", name = "gradle-nullaway-plugin", version.ref = "nullawayPlugin" }
118120
# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan
119121
paguro = { group = "org.organicdesign", name = "Paguro", version.ref = "paguro" }
122+
protobuf = { group = "com.google.protobuf", name = "protobuf-java", version.ref = "protobuf" }
123+
protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
120124
pklConfigJavaAll025 = { group = "org.pkl-lang", name = "pkl-config-java-all", version = "0.25.0" }
121125
shadowPlugin = { group = "com.gradleup.shadow", name = "com.gradleup.shadow.gradle.plugin", version.ref = "shadowPlugin" }
122126
slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
@@ -125,6 +129,7 @@ snakeYaml = { group = "org.snakeyaml", name = "snakeyaml-engine", version.ref =
125129
spotlessPlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessPlugin"}
126130
svm = { group = "org.graalvm.nativeimage", name = "svm", version.ref = "graalVmJdkVersion" }
127131
truffleApi = { group = "org.graalvm.truffle", name = "truffle-api", version.ref = "graalVm" }
132+
truffleProfiler = { group = "org.graalvm.tools", name = "profiler-tool", version.ref = "graalVm" }
128133
truffleDslProcessor = { group = "org.graalvm.truffle", name = "truffle-dsl-processor", version.ref = "graalVm" }
129134
truffleSvm = { group = "org.graalvm.nativeimage", name = "truffle-runtime-svm", version.ref = "graalVmJdkVersion" }
130135
truffleRuntime = { group = "org.graalvm.truffle", name = "truffle-runtime", version.ref = "graalVm" }
@@ -135,10 +140,11 @@ checksum = { id = "org.gradle.crypto.checksum", version.ref = "checksumPlugin" }
135140
#noinspection UnusedVersionCatalogEntry
136141
errorProne = { id = "net.ltgt.errorprone", version.ref = "errorPronePlugin" }
137142
ideaExt = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "ideaExtPlugin" }
143+
jextract = { id = "de.infolektuell.jextract", version.ref = "jextractPlugin" }
138144
jmh = { id = "me.champeau.jmh", version.ref = "jmhPlugin" }
139145
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinToolchain" }
140146
nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublishPlugin" }
141147
#noinspection UnusedVersionCatalogEntry
142148
nullaway = { id = "net.ltgt.nullaway", version.ref = "nullawayPlugin" }
143149
shadow = { id = "com.gradleup.shadow", version.ref = "shadowPlugin" }
144-
jextract = { id = "de.infolektuell.jextract", version.ref = "jextractPlugin" }
150+
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }

‎pkl-cli/pkl-cli.gradle.kts‎

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@ dependencies {
6161
implementation(libs.jlineTerminalJni)
6262
implementation(projects.pklServer)
6363
implementation(projects.pklFormatter)
64+
implementation(projects.pklProfiler)
65+
implementation(libs.truffleProfiler)
6466
implementation(libs.clikt)
6567

6668
testImplementation(projects.pklCommonsTest)
6769
testImplementation(libs.wiremock)
70+
testImplementation(libs.protobuf)
6871

6972
// used by `pklJavaExecutable` plugin (ideally this would be inferred automatically)
7073
firstPartySourcesJars(project(":pkl-cli", "sourcesJar"))
@@ -74,6 +77,7 @@ dependencies {
7477
firstPartySourcesJars(project(":pkl-formatter", "sourcesJar"))
7578
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
7679
firstPartySourcesJars(project(":pkl-server", "sourcesJar"))
80+
firstPartySourcesJars(project(":pkl-profiler", "sourcesJar"))
7781
}
7882

7983
tasks.jar {
@@ -83,6 +87,9 @@ tasks.jar {
8387

8488
tasks.javadoc { enabled = false }
8589

90+
// has its own dedicated task; see `testProfilerOutput` and `testNativeProfilerOutput` below
91+
tasks.test { exclude("**/SubprocessProfilerOutputTest.class") }
92+
8693
tasks.shadowJar {
8794
archiveFileName.set("jpkl")
8895

@@ -105,6 +112,8 @@ val testJavaExecutable =
105112
// executable;
106113
// to verify that, we don't want to include them here)
107114
(configurations.testRuntimeClasspath.get() - configurations.runtimeClasspath.get())
115+
// has its own dedicated task; see `testProfilerOutput` and `testNativeProfilerOutput` below
116+
exclude("**/SubprocessProfilerOutputTest.class")
108117
}
109118

110119
// Setup `testJavaExecutable` tasks for multi-JDK testing.
@@ -162,18 +171,97 @@ val testEvalJavaExecutableOnOtherJdks =
162171
}
163172
}
164173

174+
val generateReachabilityMetadata =
175+
tasks.register<JavaExec>("generateReachabilityMetadata") {
176+
dependsOn(tasks.javaExecutable)
177+
178+
// The native-image-agent requires GraalVM's java, not a regular JDK.
179+
val graalVm =
180+
if (buildInfo.arch == Target.Arch.AMD64) buildInfo.graalVmAmd64 else buildInfo.graalVmAarch64
181+
dependsOn(
182+
if (buildInfo.arch == Target.Arch.AMD64) ":installGraalVmAmd64" else ":installGraalVmAarch64"
183+
)
184+
185+
executable(
186+
File(graalVm.baseDir)
187+
.resolve("bin")
188+
.resolve(if (buildInfo.os.isWindows) "java.exe" else "java")
189+
.absolutePath
190+
)
191+
192+
val outputDir = layout.buildDirectory.dir("generated-reachability-metadata")
193+
val outputFile = outputDir.map {
194+
it.asFile.resolve("META-INF/native-image/org.pkl-lang/pkl-cli/reachability-metadata.json")
195+
}
196+
val profilerOutput = layout.buildDirectory.file("tmp/profile.pb.gz")
197+
outputs.file(outputFile)
198+
199+
classpath = tasks.javaExecutable.get().outputs.files
200+
201+
// invoke the profiler to include metadata for protobuf serdes
202+
args("eval", "--profile-cpu-output", profilerOutput.get().asFile.absolutePath, "pkl:settings")
203+
setStandardOutput(OutputStream.nullOutputStream())
204+
205+
jvmArgumentProviders.add(
206+
CommandLineArgumentProvider {
207+
buildList {
208+
add("-agentlib:native-image-agent=config-output-dir=${outputFile.get().parent}")
209+
}
210+
}
211+
)
212+
213+
doLast {
214+
if (!outputFile.get().exists()) {
215+
throw GradleException("Did not create expected file: ${outputFile.get()}")
216+
}
217+
}
218+
}
219+
220+
val testProfilerOutput =
221+
tasks.register<Test>("testProfilerOutput") {
222+
group = "verification"
223+
dependsOn(tasks.javaExecutable)
224+
testClassesDirs = sourceSets.test.get().output.classesDirs
225+
classpath = sourceSets.test.get().runtimeClasspath
226+
include("**/SubprocessProfilerOutputTest.class")
227+
systemProperty(
228+
"org.pkl.cli.testJar",
229+
tasks.javaExecutable.get().outputs.files.singleFile.absolutePath,
230+
)
231+
}
232+
233+
val testNativeProfilerOutput =
234+
tasks.register<Test>("testNativeProfilerOutput") {
235+
group = "verification"
236+
dependsOn(tasks.assembleNative)
237+
testClassesDirs = sourceSets.test.get().output.classesDirs
238+
classpath = sourceSets.test.get().runtimeClasspath
239+
include("**/SubprocessProfilerOutputTest.class")
240+
systemProperty(
241+
"org.pkl.cli.testExecutable",
242+
tasks.assembleNative.get().outputs.files.singleFile.absolutePath,
243+
)
244+
}
245+
246+
tasks.withType<NativeImageBuild>().configureEach {
247+
dependsOn(generateReachabilityMetadata)
248+
classpath.from(layout.buildDirectory.dir("generated-reachability-metadata"))
249+
}
250+
165251
tasks.check {
166252
dependsOn(
167253
testJavaExecutable,
168254
testJavaExecutableOnOtherJdks,
169255
testEvalJavaExecutable,
170256
testEvalJavaExecutableOnOtherJdks,
257+
testProfilerOutput,
171258
)
172259
}
173260

174261
tasks.checkNative {
175262
dependsOn(":pkl-core:checkNative")
176263
dependsOn(":pkl-server:checkNative")
264+
dependsOn(testNativeProfilerOutput)
177265
}
178266

179267
executable {

‎pkl-cli/src/main/kotlin/org/pkl/cli/CliCommandRunner.kt‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.github.ajalt.clikt.core.*
2121
import com.github.ajalt.clikt.parameters.arguments.*
2222
import com.github.ajalt.clikt.parameters.options.*
2323
import com.github.ajalt.clikt.parameters.types.int
24+
import com.oracle.truffle.api.TruffleOptions
2425
import java.io.OutputStream
2526
import java.net.URI
2627
import java.nio.file.Path
@@ -40,6 +41,8 @@ import org.pkl.core.FileOutput
4041
import org.pkl.core.ModuleSource.uri
4142
import org.pkl.core.PklBugException
4243
import org.pkl.core.PklException
44+
import org.pkl.core.ProfilerOptions
45+
import org.pkl.core.runtime.VmUtils
4346
import org.pkl.core.util.IoUtils
4447

4548
class CliCommandRunner
@@ -51,15 +54,18 @@ constructor(
5154
private val args: List<String>,
5255
private val outputStream: OutputStream = System.out,
5356
private val errStream: OutputStream = System.err,
57+
private val profilerOptions: ProfilerOptions = ProfilerOptions.DEFAULT,
5458
) : CliCommand(options) {
55-
5659
override fun doRun() {
5760
val builder = evaluatorBuilder()
5861
try {
59-
evalCmd(builder)
62+
withCpuProfiler(profilerOptions) { evalCmd(builder) }
6063
} finally {
6164
Closeables.closeQuietly(builder.moduleKeyFactories)
6265
Closeables.closeQuietly(builder.resourceReaders)
66+
if (TruffleOptions.AOT) {
67+
VmUtils.closeEngine()
68+
}
6369
}
6470
}
6571

‎pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.pkl.cli
1717

18+
import com.oracle.truffle.api.TruffleOptions
1819
import java.io.File
1920
import java.io.InputStream
2021
import java.io.OutputStream
@@ -99,14 +100,19 @@ constructor(
99100
override fun doRun() {
100101
val builder = evaluatorBuilder()
101102
try {
102-
if (options.multipleFileOutputPath != null) {
103-
writeMultipleFileOutput(builder)
104-
} else {
105-
writeOutput(builder)
103+
withCpuProfiler(options.profilerOptions) {
104+
if (options.multipleFileOutputPath != null) {
105+
writeMultipleFileOutput(builder)
106+
} else {
107+
writeOutput(builder)
108+
}
106109
}
107110
} finally {
108111
Closeables.closeQuietly(builder.moduleKeyFactories)
109112
Closeables.closeQuietly(builder.resourceReaders)
113+
if (TruffleOptions.AOT) {
114+
VmUtils.closeEngine()
115+
}
110116
}
111117
}
112118

‎pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.pkl.cli
1717

1818
import org.pkl.commons.cli.CliBaseOptions
19+
import org.pkl.core.ProfilerOptions
1920

2021
/** Configuration options for [CliEvaluator]. */
2122
data class CliEvaluatorOptions(
@@ -77,6 +78,13 @@ data class CliEvaluatorOptions(
7778
* If unset, the module's `output.bytes` property is evaluated.
7879
*/
7980
val expression: String? = null,
81+
82+
/**
83+
* CPU/Memory profiling options.
84+
*
85+
* WARN: This is an internal option only. Profiling is currently only supported via CLI flags.
86+
*/
87+
val profilerOptions: ProfilerOptions = ProfilerOptions.DEFAULT,
8088
) {
8189

8290
companion object {

0 commit comments

Comments
 (0)