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>
This commit is contained in:
authored and GitHub committed 2026-10-15 15:57:55 +00:00
1 parent d062312df9
commit f96fba00e1
48 files changed
+1480 -38

No files matched your search

+53
View File
@@ -408,3 +408,56 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
28) protobuf-java
License URL: https://github.com/protocolbuffers/protobuf/blob/main/LICENSE
Copyright 2008 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Code generated by the Protocol Buffer compiler is owned by the owner
of the input file used when generating it. This code is not
standalone and requires a support library to be linked with it. This
support library is itself covered by the above license.
29) pprof
License URL: https://github.com/google/pprof/blob/main/LICENSE
Copyright 2016 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
@@ -227,6 +227,8 @@ abstract class NativeImageBuild : DefaultTask() {
add("--initialize-at-run-time=org.pkl.core.util.DebugLogger")
// needed for jline-terminal-jni
add("--initialize-at-run-time=org.jline.nativ,org.jline.terminal.impl.jni")
// needed for protobuf-java
add("--initialize-at-run-time=com.google.protobuf.UnsafeUtil")
add("--no-fallback")
add("-H:IncludeResources=org/pkl/core/stdlib/.*\\.pkl")
add("-H:IncludeResources=org/jline/utils/.*")
@@ -66,6 +66,11 @@ val relocations =
// pkl-codegen-kotlin dependencies
"com.squareup.kotlinpoet." to "org.pkl.thirdparty.kotlinpoet.",
// pkl-profiler dependencies
"com.google.protobuf." to "org.pkl.thirdparty.com.google.protobuf.",
"com.google.perftools." to "org.pkl.thirdparty.com.google.perftools.",
"google.protobuf." to "org.pkl.thirdparty.google.protobuf.",
)
for ((key, value) in relocations) {
+18
View File
@@ -450,6 +450,24 @@ When enabled, type constraint failures will show intermediate values in the asse
Use `--no-power-assertions` to disable this feature if you prefer simpler output or better performance.
====
[[profile-cpu-output]]
.--profile-cpu-output
[%collapsible]
====
Default: (none) +
The file to write profiler output to. Omitting this option disables CPU profiling entirely. +
The CPU profiler writes output in https://github.com/google/pprof[pprof] format.
====
[[profile-cpu-sample-period]]
.--profile-cpu-sample-period
[%collapsible]
====
Default: 10 +
The sampling duration for the CPU sampler, in milliseconds.
====
This command also takes <<common-options, common options>>.
[[command-server]]
+7 -1
View File
@@ -62,6 +62,8 @@ nullaway = "0.13.7"
nullawayPlugin = "3.0.0"
nuValidator = "26.7.3"
paguro = "3.10.3"
protobufPlugin = "0.10.0"
protobuf = "4.35.1"
shadowPlugin = "9.5.1"
slf4j = "2.0.20"
snakeYaml = "3.0.1"
@@ -117,6 +119,8 @@ nullaway = { group = "com.uber.nullaway", name = "nullaway", version.ref = "null
nullawayPlugin = { group = "net.ltgt.gradle", name = "gradle-nullaway-plugin", version.ref = "nullawayPlugin" }
# to be replaced with https://github.com/usethesource/capsule or https://github.com/lacuna/bifurcan
paguro = { group = "org.organicdesign", name = "Paguro", version.ref = "paguro" }
protobuf = { group = "com.google.protobuf", name = "protobuf-java", version.ref = "protobuf" }
protoc = { group = "com.google.protobuf", name = "protoc", version.ref = "protobuf" }
pklConfigJavaAll025 = { group = "org.pkl-lang", name = "pkl-config-java-all", version = "0.25.0" }
shadowPlugin = { group = "com.gradleup.shadow", name = "com.gradleup.shadow.gradle.plugin", version.ref = "shadowPlugin" }
slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
@@ -125,6 +129,7 @@ snakeYaml = { group = "org.snakeyaml", name = "snakeyaml-engine", version.ref =
spotlessPlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotlessPlugin"}
svm = { group = "org.graalvm.nativeimage", name = "svm", version.ref = "graalVmJdkVersion" }
truffleApi = { group = "org.graalvm.truffle", name = "truffle-api", version.ref = "graalVm" }
truffleProfiler = { group = "org.graalvm.tools", name = "profiler-tool", version.ref = "graalVm" }
truffleDslProcessor = { group = "org.graalvm.truffle", name = "truffle-dsl-processor", version.ref = "graalVm" }
truffleSvm = { group = "org.graalvm.nativeimage", name = "truffle-runtime-svm", version.ref = "graalVmJdkVersion" }
truffleRuntime = { group = "org.graalvm.truffle", name = "truffle-runtime", version.ref = "graalVm" }
@@ -135,10 +140,11 @@ checksum = { id = "org.gradle.crypto.checksum", version.ref = "checksumPlugin" }
#noinspection UnusedVersionCatalogEntry
errorProne = { id = "net.ltgt.errorprone", version.ref = "errorPronePlugin" }
ideaExt = { id = "org.jetbrains.gradle.plugin.idea-ext", version.ref = "ideaExtPlugin" }
jextract = { id = "de.infolektuell.jextract", version.ref = "jextractPlugin" }
jmh = { id = "me.champeau.jmh", version.ref = "jmhPlugin" }
kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinToolchain" }
nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublishPlugin" }
#noinspection UnusedVersionCatalogEntry
nullaway = { id = "net.ltgt.nullaway", version.ref = "nullawayPlugin" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadowPlugin" }
jextract = { id = "de.infolektuell.jextract", version.ref = "jextractPlugin" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
+88
View File
@@ -61,10 +61,13 @@ dependencies {
implementation(libs.jlineTerminalJni)
implementation(projects.pklServer)
implementation(projects.pklFormatter)
implementation(projects.pklProfiler)
implementation(libs.truffleProfiler)
implementation(libs.clikt)
testImplementation(projects.pklCommonsTest)
testImplementation(libs.wiremock)
testImplementation(libs.protobuf)
// used by `pklJavaExecutable` plugin (ideally this would be inferred automatically)
firstPartySourcesJars(project(":pkl-cli", "sourcesJar"))
@@ -74,6 +77,7 @@ dependencies {
firstPartySourcesJars(project(":pkl-formatter", "sourcesJar"))
firstPartySourcesJars(project(":pkl-parser", "sourcesJar"))
firstPartySourcesJars(project(":pkl-server", "sourcesJar"))
firstPartySourcesJars(project(":pkl-profiler", "sourcesJar"))
}
tasks.jar {
@@ -83,6 +87,9 @@ tasks.jar {
tasks.javadoc { enabled = false }
// has its own dedicated task; see `testProfilerOutput` and `testNativeProfilerOutput` below
tasks.test { exclude("**/SubprocessProfilerOutputTest.class") }
tasks.shadowJar {
archiveFileName.set("jpkl")
@@ -105,6 +112,8 @@ val testJavaExecutable =
// executable;
// to verify that, we don't want to include them here)
(configurations.testRuntimeClasspath.get() - configurations.runtimeClasspath.get())
// has its own dedicated task; see `testProfilerOutput` and `testNativeProfilerOutput` below
exclude("**/SubprocessProfilerOutputTest.class")
}
// Setup `testJavaExecutable` tasks for multi-JDK testing.
@@ -162,18 +171,97 @@ val testEvalJavaExecutableOnOtherJdks =
}
}
val generateReachabilityMetadata =
tasks.register<JavaExec>("generateReachabilityMetadata") {
dependsOn(tasks.javaExecutable)
// The native-image-agent requires GraalVM's java, not a regular JDK.
val graalVm =
if (buildInfo.arch == Target.Arch.AMD64) buildInfo.graalVmAmd64 else buildInfo.graalVmAarch64
dependsOn(
if (buildInfo.arch == Target.Arch.AMD64) ":installGraalVmAmd64" else ":installGraalVmAarch64"
)
executable(
File(graalVm.baseDir)
.resolve("bin")
.resolve(if (buildInfo.os.isWindows) "java.exe" else "java")
.absolutePath
)
val outputDir = layout.buildDirectory.dir("generated-reachability-metadata")
val outputFile = outputDir.map {
it.asFile.resolve("META-INF/native-image/org.pkl-lang/pkl-cli/reachability-metadata.json")
}
val profilerOutput = layout.buildDirectory.file("tmp/profile.pb.gz")
outputs.file(outputFile)
classpath = tasks.javaExecutable.get().outputs.files
// invoke the profiler to include metadata for protobuf serdes
args("eval", "--profile-cpu-output", profilerOutput.get().asFile.absolutePath, "pkl:settings")
setStandardOutput(OutputStream.nullOutputStream())
jvmArgumentProviders.add(
CommandLineArgumentProvider {
buildList {
add("-agentlib:native-image-agent=config-output-dir=${outputFile.get().parent}")
}
}
)
doLast {
if (!outputFile.get().exists()) {
throw GradleException("Did not create expected file: ${outputFile.get()}")
}
}
}
val testProfilerOutput =
tasks.register<Test>("testProfilerOutput") {
group = "verification"
dependsOn(tasks.javaExecutable)
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
include("**/SubprocessProfilerOutputTest.class")
systemProperty(
"org.pkl.cli.testJar",
tasks.javaExecutable.get().outputs.files.singleFile.absolutePath,
)
}
val testNativeProfilerOutput =
tasks.register<Test>("testNativeProfilerOutput") {
group = "verification"
dependsOn(tasks.assembleNative)
testClassesDirs = sourceSets.test.get().output.classesDirs
classpath = sourceSets.test.get().runtimeClasspath
include("**/SubprocessProfilerOutputTest.class")
systemProperty(
"org.pkl.cli.testExecutable",
tasks.assembleNative.get().outputs.files.singleFile.absolutePath,
)
}
tasks.withType<NativeImageBuild>().configureEach {
dependsOn(generateReachabilityMetadata)
classpath.from(layout.buildDirectory.dir("generated-reachability-metadata"))
}
tasks.check {
dependsOn(
testJavaExecutable,
testJavaExecutableOnOtherJdks,
testEvalJavaExecutable,
testEvalJavaExecutableOnOtherJdks,
testProfilerOutput,
)
}
tasks.checkNative {
dependsOn(":pkl-core:checkNative")
dependsOn(":pkl-server:checkNative")
dependsOn(testNativeProfilerOutput)
}
executable {
@@ -21,6 +21,7 @@ import com.github.ajalt.clikt.core.*
import com.github.ajalt.clikt.parameters.arguments.*
import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.int
import com.oracle.truffle.api.TruffleOptions
import java.io.OutputStream
import java.net.URI
import java.nio.file.Path
@@ -40,6 +41,8 @@ import org.pkl.core.FileOutput
import org.pkl.core.ModuleSource.uri
import org.pkl.core.PklBugException
import org.pkl.core.PklException
import org.pkl.core.ProfilerOptions
import org.pkl.core.runtime.VmUtils
import org.pkl.core.util.IoUtils
class CliCommandRunner
@@ -51,15 +54,18 @@ constructor(
private val args: List<String>,
private val outputStream: OutputStream = System.out,
private val errStream: OutputStream = System.err,
private val profilerOptions: ProfilerOptions = ProfilerOptions.DEFAULT,
) : CliCommand(options) {
override fun doRun() {
val builder = evaluatorBuilder()
try {
evalCmd(builder)
withCpuProfiler(profilerOptions) { evalCmd(builder) }
} finally {
Closeables.closeQuietly(builder.moduleKeyFactories)
Closeables.closeQuietly(builder.resourceReaders)
if (TruffleOptions.AOT) {
VmUtils.closeEngine()
}
}
}
@@ -15,6 +15,7 @@
*/
package org.pkl.cli
import com.oracle.truffle.api.TruffleOptions
import java.io.File
import java.io.InputStream
import java.io.OutputStream
@@ -99,14 +100,19 @@ constructor(
override fun doRun() {
val builder = evaluatorBuilder()
try {
if (options.multipleFileOutputPath != null) {
writeMultipleFileOutput(builder)
} else {
writeOutput(builder)
withCpuProfiler(options.profilerOptions) {
if (options.multipleFileOutputPath != null) {
writeMultipleFileOutput(builder)
} else {
writeOutput(builder)
}
}
} finally {
Closeables.closeQuietly(builder.moduleKeyFactories)
Closeables.closeQuietly(builder.resourceReaders)
if (TruffleOptions.AOT) {
VmUtils.closeEngine()
}
}
}
@@ -16,6 +16,7 @@
package org.pkl.cli
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.core.ProfilerOptions
/** Configuration options for [CliEvaluator]. */
data class CliEvaluatorOptions(
@@ -77,6 +78,13 @@ data class CliEvaluatorOptions(
* If unset, the module's `output.bytes` property is evaluated.
*/
val expression: String? = null,
/**
* CPU/Memory profiling options.
*
* WARN: This is an internal option only. Profiling is currently only supported via CLI flags.
*/
val profilerOptions: ProfilerOptions = ProfilerOptions.DEFAULT,
) {
companion object {
@@ -15,6 +15,7 @@
*/
package org.pkl.cli
import com.oracle.truffle.api.TruffleOptions
import java.io.StringWriter
import java.io.Writer
import org.pkl.commons.cli.*
@@ -22,7 +23,9 @@ import org.pkl.core.Closeables
import org.pkl.core.EvaluatorBuilder
import org.pkl.core.ModuleSource.uri
import org.pkl.core.PklException
import org.pkl.core.ProfilerOptions
import org.pkl.core.TestResults
import org.pkl.core.runtime.VmUtils
import org.pkl.core.stdlib.test.report.JUnitReporter
import org.pkl.core.stdlib.test.report.MinimalReporter
import org.pkl.core.stdlib.test.report.SpecReporter
@@ -35,15 +38,18 @@ constructor(
private val testOptions: CliTestOptions,
private val consoleWriter: Writer = System.out.writer(),
private val errWriter: Writer = System.err.writer(),
private val profilerOptions: ProfilerOptions = ProfilerOptions.DEFAULT,
) : CliCommand(options) {
override fun doRun() {
val builder = evaluatorBuilder()
try {
evalTest(builder)
withCpuProfiler(profilerOptions) { evalTest(builder) }
} finally {
Closeables.closeQuietly(builder.moduleKeyFactories)
Closeables.closeQuietly(builder.resourceReaders)
if (TruffleOptions.AOT) {
VmUtils.closeEngine()
}
}
}
@@ -16,6 +16,7 @@
package org.pkl.cli.commands
import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.parameters.groups.provideDelegate
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
@@ -23,6 +24,7 @@ import com.github.ajalt.clikt.parameters.options.validate
import org.pkl.cli.CliEvaluator
import org.pkl.cli.CliEvaluatorOptions
import org.pkl.commons.cli.commands.ModulesCommand
import org.pkl.commons.cli.commands.ProfileOptions
import org.pkl.commons.cli.commands.single
class EvalCommand : ModulesCommand(name = "eval", helpLink = helpLink) {
@@ -81,21 +83,25 @@ class EvalCommand : ModulesCommand(name = "eval", helpLink = helpLink) {
)
.flag("--no-power-assertions", default = true, defaultForHelp = "enabled")
val profileOptions: ProfileOptions by ProfileOptions()
override fun run() {
val cliBaseOptions =
baseOptions.baseOptions(
modules,
projectOptions,
testMode = testMode,
powerAssertionsEnabled = powerAssertionsEnabled,
)
val options =
CliEvaluatorOptions(
base =
baseOptions.baseOptions(
modules,
projectOptions,
testMode = testMode,
powerAssertionsEnabled = powerAssertionsEnabled,
),
base = cliBaseOptions,
outputPath = outputPath,
outputFormat = baseOptions.format,
moduleOutputSeparator = moduleOutputSeparator,
multipleFileOutputPath = multipleFileOutputPath,
expression = expression ?: CliEvaluatorOptions.defaults.expression,
profilerOptions = profileOptions.toOptions(cliBaseOptions.normalizedWorkingDir),
)
CliEvaluator(options).run()
}
@@ -30,6 +30,7 @@ import java.net.URI
import org.pkl.cli.CliCommandRunner
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.BaseOptions
import org.pkl.commons.cli.commands.ProfileOptions
import org.pkl.commons.cli.commands.ProjectOptions
class RunCommand : BaseCommand(name = "run", helpLink = helpLink) {
@@ -61,6 +62,8 @@ class RunCommand : BaseCommand(name = "run", helpLink = helpLink) {
private val projectOptions by ProjectOptions()
private val profileOptions by ProfileOptions()
override fun run() {
// if no module is specified but --help is show help, otherwise error becuase module is missing
if (module == null)
@@ -75,11 +78,13 @@ class RunCommand : BaseCommand(name = "run", helpLink = helpLink) {
else reservedFlagShortNames.add(it.trimStart('-'))
}
}
val cliBaseOptions = baseOptions.baseOptions(listOf(module!!), projectOptions)
CliCommandRunner(
baseOptions.baseOptions(listOf(module!!), projectOptions),
cliBaseOptions,
reservedFlagNames,
reservedFlagShortNames,
if (showHelp) args + listOf("--help") else args,
profilerOptions = profileOptions.toOptions(cliBaseOptions.normalizedWorkingDir),
)
.run()
}
@@ -26,6 +26,7 @@ import java.net.URI
import org.pkl.cli.CliTestRunner
import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.BaseOptions
import org.pkl.commons.cli.commands.ProfileOptions
import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.TestOptions
@@ -45,6 +46,8 @@ class TestCommand : BaseCommand(name = "test", helpLink = helpLink) {
private val testOptions by TestOptions()
private val profileOptions by ProfileOptions()
private val powerAssertionsEnabled: Boolean by
option(
names = arrayOf("--power-assertions"),
@@ -53,14 +56,16 @@ class TestCommand : BaseCommand(name = "test", helpLink = helpLink) {
.flag("--no-power-assertions", default = true, defaultForHelp = "enabled")
override fun run() {
val cliBaseOptions =
baseOptions.baseOptions(
modules,
projectOptions,
powerAssertionsEnabled = powerAssertionsEnabled,
)
CliTestRunner(
options =
baseOptions.baseOptions(
modules,
projectOptions,
powerAssertionsEnabled = powerAssertionsEnabled,
),
options = cliBaseOptions,
testOptions = testOptions.cliTestOptions,
profilerOptions = profileOptions.toOptions(cliBaseOptions.normalizedWorkingDir),
)
.run()
}
@@ -0,0 +1,38 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.cli
import org.pkl.core.ProfilerOptions
import org.pkl.core.runtime.VmUtils
import org.pkl.profiler.CpuProfiler
import org.pkl.profiler.CpuProfilerInstrument
/**
* Runs [body], sampling it with the CPU profiler if [options] enables CPU profiling.
*
* Profiler output is written when [body] finishes, whether normally or exceptionally.
*/
internal inline fun <T> withCpuProfiler(options: ProfilerOptions, body: () -> T): T {
if (!options.cpu.isEnabled) return body()
val profiler =
VmUtils.getEngine().instruments[CpuProfilerInstrument.ID]?.lookup(CpuProfiler::class.java)!!
profiler.start(options.cpu)
try {
return body()
} finally {
profiler.stop()
}
}
@@ -0,0 +1,199 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.cli
import com.google.perftools.profiles.ProfileProto.Profile
import java.io.ByteArrayOutputStream
import java.io.StringWriter
import java.net.URI
import java.nio.file.Path
import java.util.zip.GZIPInputStream
import kotlin.io.path.exists
import kotlin.io.path.inputStream
import kotlin.io.path.writeText
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliTestOptions
import org.pkl.core.ProfilerOptions
/**
* Exercises the CPU profiler by calling into the CLI classes in-process.
*
* See [SubprocessProfilerOutputTest] for tests that run the packaged executables.
*/
class ProfilerOutputTest {
private fun profilerOptions(outputFile: Path) =
ProfilerOptions(ProfilerOptions.Cpu(outputFile, 1))
private fun fib(name: String) =
"function $name(n) = if (n < 2) n else $name(n - 1) + $name(n - 2)"
private fun evalCode(name: String) =
"""
${fib(name)}
x = $name(30)
"""
.trimIndent()
private fun testCode(name: String) =
"""
amends "pkl:test"
local ${fib(name)}
facts {
["fib"] {
$name(30) > 0
}
}
"""
.trimIndent()
private fun runCode(name: String) =
"""
amends "pkl:Command"
command {
name = "do-run"
}
local ${fib(name)}
output {
text = $name(30).toString()
}
"""
.trimIndent()
private fun baseOptions(source: Path) =
CliBaseOptions(sourceModules = listOf(source.toUri()), settings = URI("pkl:settings"))
private fun eval(tempDir: Path, name: String, outputFile: Path) {
val source = tempDir.resolve("$name.pkl").also { it.writeText(evalCode(name)) }
val options =
CliEvaluatorOptions(baseOptions(source), profilerOptions = profilerOptions(outputFile))
CliEvaluator(options, outputStream = ByteArrayOutputStream()).run()
}
private fun test(tempDir: Path, name: String, outputFile: Path) {
val source = tempDir.resolve("$name.pkl").also { it.writeText(testCode(name)) }
CliTestRunner(
baseOptions(source),
CliTestOptions(),
consoleWriter = StringWriter(),
errWriter = StringWriter(),
profilerOptions = profilerOptions(outputFile),
)
.run()
}
private fun run(tempDir: Path, name: String, outputFile: Path) {
val source = tempDir.resolve("$name.pkl").also { it.writeText(runCode(name)) }
CliCommandRunner(
baseOptions(source),
emptySet(),
emptySet(),
emptyList(),
outputStream = ByteArrayOutputStream(),
errStream = ByteArrayOutputStream(),
profilerOptions = profilerOptions(outputFile),
)
.run()
}
private fun readProfile(outputFile: Path): Profile =
GZIPInputStream(outputFile.inputStream()).use { Profile.parseFrom(it) }
private fun Profile.functionNames() = functionList.map { stringTableList[it.name.toInt()] }
/** Smoke test of the profile's structure, and that it contains samples of [fibName]. */
private fun assertValidProfile(outputFile: Path, fibName: String) {
val profile = readProfile(outputFile)
assertThat(profile.stringTableList.first()).isEmpty() // string_table[0] must be ""
assertThat(profile.sampleTypeList).isNotEmpty
assertThat(profile.functionList).isNotEmpty
assertThat(profile.functionNames()).anyMatch { it.contains(fibName) }
}
@Test
fun `pkl eval`(@TempDir tempDir: Path) {
val outputFile = tempDir.resolve("profile.pb.gz")
eval(tempDir, "evalFib", outputFile)
assertValidProfile(outputFile, "evalFib")
}
@Test
fun `pkl test`(@TempDir tempDir: Path) {
val outputFile = tempDir.resolve("profile.pb.gz")
test(tempDir, "testFib", outputFile)
assertValidProfile(outputFile, "testFib")
}
@Test
fun `pkl run`(@TempDir tempDir: Path) {
val outputFile = tempDir.resolve("profile.pb.gz")
run(tempDir, "runFib", outputFile)
assertValidProfile(outputFile, "runFib")
}
@Test
fun `profiler can be restarted, and runs don't share samples`(@TempDir tempDir: Path) {
val firstOutput = tempDir.resolve("first.pb.gz")
val secondOutput = tempDir.resolve("second.pb.gz")
val thirdOutput = tempDir.resolve("third.pb.gz")
eval(tempDir, "firstFib", firstOutput)
test(tempDir, "secondFib", secondOutput)
run(tempDir, "thirdFib", thirdOutput)
assertValidProfile(firstOutput, "firstFib")
assertValidProfile(secondOutput, "secondFib")
assertValidProfile(thirdOutput, "thirdFib")
// samples from earlier runs must not leak into later ones
assertThat(readProfile(secondOutput).functionNames()).noneMatch { it.contains("firstFib") }
assertThat(readProfile(thirdOutput).functionNames()).noneMatch {
it.contains("firstFib") || it.contains("secondFib")
}
}
@Test
fun `profiler restarts after a failed evaluation`(@TempDir tempDir: Path) {
val failingSource = tempDir.resolve("failing.pkl").also { it.writeText("x = throw(\"boom\")") }
val failingOutput = tempDir.resolve("failing.pb.gz")
val options =
CliEvaluatorOptions(
baseOptions(failingSource),
profilerOptions = profilerOptions(failingOutput),
)
runCatching { CliEvaluator(options, outputStream = ByteArrayOutputStream()).run() }
.also { assertThat(it.isFailure).isTrue() }
val outputFile = tempDir.resolve("profile.pb.gz")
eval(tempDir, "afterFailureFib", outputFile)
assertValidProfile(outputFile, "afterFailureFib")
}
@Test
fun `no output is written if profiling is not enabled`(@TempDir tempDir: Path) {
val source = tempDir.resolve("noProfile.pkl").also { it.writeText(evalCode("noProfileFib")) }
CliEvaluator(CliEvaluatorOptions(baseOptions(source)), outputStream = ByteArrayOutputStream())
.run()
assertThat(tempDir.resolve("profile.pb.gz").exists()).isFalse()
}
}
@@ -0,0 +1,131 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.cli
import com.google.perftools.profiles.ProfileProto.Profile
import java.nio.file.Path
import java.util.zip.GZIPInputStream
import kotlin.io.path.absolutePathString
import kotlin.io.path.inputStream
import kotlin.io.path.writeText
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
/**
* Exercises the CPU profiler end-to-end by running the packaged executable jar (or native
* executable) as a subprocess.
*
* Run this test using `./gradlew pkl-cli:testProfilerOutput` (executable jar) or `./gradlew
* pkl-cli:testNativeProfilerOutput` (native executable).
*
* To use an IntelliJ JUnit run configuration, configure the VM options and add
* `-Dorg.pkl.cli.testJar=<path>` (or `-Dorg.pkl.cli.testExecutable=<path>`).
*/
class SubprocessProfilerOutputTest {
/**
* The command that launches Pkl: the native executable if `org.pkl.cli.testExecutable` is set,
* otherwise the executable jar given by `org.pkl.cli.testJar`.
*/
private fun launcher(): Array<String> {
System.getProperty("org.pkl.cli.testExecutable")?.let {
return arrayOf(it)
}
val jar =
checkNotNull(System.getProperty("org.pkl.cli.testJar")) {
"neither system property `org.pkl.cli.testExecutable` nor `org.pkl.cli.testJar` is set"
}
val javaBin = ProcessHandle.current().info().command().orElseThrow()
return arrayOf(javaBin, "-jar", jar)
}
private fun runTest(tempDir: Path, pklCode: String, command: Array<String>) {
val sourceFile = tempDir.resolve("profiled.pkl").also { it.writeText(pklCode) }
val outputFile = tempDir.resolve("profile.pb.gz")
val process =
ProcessBuilder(
*launcher(),
*command,
"--profile-cpu-output",
outputFile.absolutePathString(),
"--profile-cpu-sample-period",
"1",
sourceFile.absolutePathString(),
)
.redirectErrorStream(true)
.start()
val output = process.inputStream.bufferedReader().readText()
val exitCode = process.waitFor()
assertThat(exitCode).withFailMessage { "process failed with output:\n$output" }.isZero()
// smoke test only
val profile = GZIPInputStream(outputFile.inputStream()).use { Profile.parseFrom(it) }
assertThat(profile.stringTableList.first()).isEmpty() // string_table[0] must be ""
assertThat(profile.sampleTypeList).isNotEmpty
assertThat(profile.functionList).isNotEmpty
}
@Test
fun `pkl eval`(@TempDir tempDir: Path) {
val pklCode =
"""
function fib(n) = if (n < 2) n else fib(n - 1) + fib(n - 2)
x = fib(30)
"""
.trimIndent()
runTest(tempDir, pklCode, arrayOf("eval"))
}
@Test
fun `pkl test`(@TempDir tempDir: Path) {
val pklCode =
"""
amends "pkl:test"
local function fib(n) = if (n < 2) n else fib(n - 1) + fib(n - 2)
facts {
["fib"] {
fib(30) > 0
}
}
"""
.trimIndent()
runTest(tempDir, pklCode, arrayOf("eval"))
}
@Test
fun `pkl run`(@TempDir tempDir: Path) {
val pklCode =
"""
amends "pkl:Command"
command {
name = "do-run"
}
local function fib(n) = if (n < 2) n else fib(n - 1) + fib(n - 2)
output {
text = fib(30).toString()
}
"""
.trimIndent()
runTest(tempDir, pklCode, arrayOf("run"))
}
}
@@ -0,0 +1,49 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.commons.cli.commands
import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.types.long
import com.github.ajalt.clikt.parameters.types.path
import java.nio.file.Path
import org.pkl.core.ProfilerOptions
class ProfileOptions : OptionGroup() {
val profileCpuOutput: Path? by
option(
names = arrayOf("--profile-cpu-output"),
help =
"The file to write CPU profiler output to. Omitting this option disables CPU profiling.",
)
.single()
.path()
val profileCpuSamplePeriod: Long by
option(
names = arrayOf("--profile-cpu-sample-period"),
help = "Duration, in milliseconds, for the CPU profiler to poll for samples.",
)
.single()
.long()
.default(ProfilerOptions.DEFAULT.cpu.samplePeriod)
fun toOptions(workingDir: Path): ProfilerOptions =
ProfilerOptions(
ProfilerOptions.Cpu(profileCpuOutput?.let(workingDir::resolve), profileCpuSamplePeriod)
)
}
@@ -0,0 +1,31 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core;
import java.nio.file.Path;
import org.jspecify.annotations.Nullable;
public record ProfilerOptions(Cpu cpu) {
public static final ProfilerOptions DEFAULT = new ProfilerOptions(Cpu.DEFAULT);
public record Cpu(@Nullable Path outputFile, long samplePeriod) {
public static final Cpu DEFAULT = new Cpu(null, 10);
public boolean isEnabled() {
return outputFile != null;
}
}
}
@@ -17,6 +17,7 @@ package org.pkl.core.ast.expression.generator;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Specialization;
import com.oracle.truffle.api.frame.FrameDescriptor;
@@ -165,6 +166,7 @@ public abstract class GeneratorForNode extends GeneratorMemberNode {
private void executeIteration(
VirtualFrame frame, Object parent, ObjectData data, Object key, Object value) {
TruffleSafepoint.poll(this);
// GraalJS uses the same implementation technique here:
// https://github.com/oracle/graaljs/blob/44a11ce6e87/graal-js/src/com.oracle.truffle.js/src/com/oracle/truffle/js/nodes/function/IterationScopeNode.java#L86-L88
var newFrame =
@@ -17,6 +17,7 @@ package org.pkl.core.ast.expression.generator;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.dsl.Bind;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Fallback;
@@ -266,6 +267,7 @@ public abstract class GeneratorObjectLiteralNode extends ObjectLiteralNode {
private ObjectData executeChildren(VirtualFrame frame, Object parent, int parentLength) {
var data = new ObjectData(parentLength);
for (var memberNode : memberNodes) {
TruffleSafepoint.poll(this);
memberNode.execute(frame, parent, data);
}
return data;
@@ -74,7 +74,7 @@ public abstract class AmendModuleNode extends SpecializedObjectLiteralNode {
module.addProperties(members);
module.setExtraStorage(moduleInfo);
moduleInfo.initAnnotations(VmUtils.evaluateAnnotations(frame, annotationNodes));
moduleInfo.initAnnotations(VmUtils.evaluateAnnotations(frame, this, annotationNodes));
return module;
}
@@ -16,6 +16,7 @@
package org.pkl.core.ast.expression.literal;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.NodeInfo;
@@ -55,6 +56,7 @@ public final class BytesLiteralNode extends ExpressionNode {
for (var i = 0; i < elements.length; i++) {
var elem = elements[i];
try {
TruffleSafepoint.poll(this);
var result = (Long) typeNode.execute(frame, elem.executeGeneric(frame));
bytes[i] = result.byteValue();
} catch (VmTypeMismatchException err) {
@@ -187,6 +187,7 @@ public abstract class ElementsEntriesLiteralNode extends SpecializedObjectLitera
addListEntries(frame, parentLength, result, keyNodes, values);
for (var i = 0; i < elements.length; i++) {
// intentionally no `TruffleSafePoint.poll()` here; no truffle node execution happening
EconomicMaps.put(result, (long) (parentLength + i), elements[i]);
}
@@ -17,6 +17,7 @@ package org.pkl.core.ast.expression.literal;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.dsl.*;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.VirtualFrame;
@@ -196,6 +197,7 @@ public abstract class EntriesLiteralNode extends SpecializedObjectLiteralNode {
EconomicMaps.putAll(result, members);
for (var i = 0; i < keyNodes.length; i++) {
TruffleSafepoint.poll(this);
var key = keyNodes[i].executeGeneric(frame);
var value = values[i];
var previousValue = EconomicMaps.put(result, key, value);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
package org.pkl.core.ast.expression.literal;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.NodeInfo;
@@ -36,6 +37,7 @@ public final class InterpolatedStringLiteralNode extends ExpressionNode {
public String executeGeneric(VirtualFrame frame) {
var builder = VmUtils.createBuilder();
for (var part : parts) {
TruffleSafepoint.poll(this);
VmUtils.appendToBuilder(builder, (String) part.executeGeneric(frame));
}
return VmUtils.builderToString(builder);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
package org.pkl.core.ast.expression.literal;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.NodeInfo;
@@ -36,6 +37,7 @@ public final class ListLiteralNode extends ExpressionNode {
public VmList executeGeneric(VirtualFrame frame) {
var builder = VmList.EMPTY.builder();
for (var element : elements) {
TruffleSafepoint.poll(this);
builder.add(element.executeGeneric(frame));
}
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
package org.pkl.core.ast.expression.literal;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.NodeInfo;
@@ -38,6 +39,7 @@ public final class MapLiteralNode extends ExpressionNode {
var builder = VmMap.builder();
for (var i = 0; i < keysAndValues.length; i += 2) {
TruffleSafepoint.poll(this);
var key = keysAndValues[i].executeGeneric(frame);
var value = keysAndValues[i + 1].executeGeneric(frame);
builder.add(key, value);
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
package org.pkl.core.ast.expression.literal;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.nodes.NodeInfo;
@@ -36,6 +37,7 @@ public final class SetLiteralNode extends ExpressionNode {
public VmSet executeGeneric(VirtualFrame frame) {
var builder = VmSet.EMPTY.builder();
for (var element : elements) {
TruffleSafepoint.poll(this);
builder.add(element.executeGeneric(frame));
}
@@ -18,6 +18,7 @@ package org.pkl.core.ast.expression.literal;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.dsl.Idempotent;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.VirtualFrame;
@@ -230,6 +231,7 @@ public abstract class SpecializedObjectLiteralNode extends ObjectLiteralNode {
for (var i = 0; i < keyNodes.length; i++) {
long index;
try {
TruffleSafepoint.poll(this);
index = keyNodes[i].executeInt(frame);
} catch (UnexpectedResultException e) {
CompilerDirectives.transferToInterpreter();
@@ -15,6 +15,7 @@
*/
package org.pkl.core.ast.expression.member;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.nodes.ExplodeLoop;
import com.oracle.truffle.api.source.SourceSection;
@@ -49,6 +50,7 @@ public abstract class AbstractInvokeMethodNode extends ExpressionNode {
try {
for (var i = 0; i < argumentNodes.length; i++) {
TruffleSafepoint.poll(this);
args[2 + i] = argumentNodes[i].executeGeneric(frame);
}
} finally {
@@ -147,7 +147,7 @@ public final class ClassNode extends ExpressionNode {
// which is guaranteed to succeed (no impact on eager vs. lazy error reporting) and easy to
// defer.
VmUtils.evaluateAnnotations(frame, annotationNodes, annotations);
VmUtils.evaluateAnnotations(frame, this, annotationNodes, annotations);
for (var node : unresolvedPropertyNodes) {
cachedClass.addProperty(node.execute(frame, cachedClass));
@@ -88,7 +88,7 @@ public final class TypeAliasNode extends ExpressionNode {
typeParameters,
frame.materialize());
VmUtils.evaluateAnnotations(frame, annotationNodes, annotations);
VmUtils.evaluateAnnotations(frame, this, annotationNodes, annotations);
initTypeAliasModule(typeAnnotationNode, module);
var bodyTypeNode = typeAnnotationNode.execute(frame);
cachedTypeAlias.initTypeCheckNode(bodyTypeNode);
@@ -87,7 +87,7 @@ public final class UnresolvedMethodNode extends UnresolvedClassMemberNode {
public ClassMethod execute(VirtualFrame frame, VmClass clazz) {
CompilerDirectives.transferToInterpreter();
var annotations = VmUtils.evaluateAnnotations(frame, annotationNodes);
var annotations = VmUtils.evaluateAnnotations(frame, this, annotationNodes);
var parameterTypeNodes =
VmUtils.resolveParameterTypes(frame, descriptor, unresolvedParameterTypeNodes);
var returnTypeNode =
@@ -158,7 +158,7 @@ public final class UnresolvedPropertyNode extends UnresolvedClassMemberNode {
public ClassProperty execute(VirtualFrame frame, VmClass clazz) {
CompilerDirectives.transferToInterpreter();
var annotations = VmUtils.evaluateAnnotations(frame, annotationNodes);
var annotations = VmUtils.evaluateAnnotations(frame, this, annotationNodes);
var typeNode =
unresolvedTypeNode == null
@@ -18,6 +18,7 @@ package org.pkl.core.ast.type;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.dsl.Fallback;
import com.oracle.truffle.api.dsl.Idempotent;
@@ -1270,6 +1271,7 @@ public abstract class TypeNode extends PklNode {
var idx = 0;
for (var elem : value) {
TruffleSafepoint.poll(this);
var result = elementTypeNode.executeLazily(frame, elem);
if (result != elem) {
ret = ret.replace(idx, result);
@@ -1283,6 +1285,7 @@ public abstract class TypeNode extends PklNode {
private Object evalListEagerly(VirtualFrame frame, VmList value) {
for (var elem : value) {
TruffleSafepoint.poll(this);
elementTypeNode.executeEagerly(frame, elem);
}
@@ -1292,6 +1295,7 @@ public abstract class TypeNode extends PklNode {
private Object evalSet(VirtualFrame frame, VmSet value) {
for (var elem : value) {
TruffleSafepoint.poll(this);
elementTypeNode.executeEagerly(frame, elem);
}
@@ -1347,6 +1351,7 @@ public abstract class TypeNode extends PklNode {
if (elementTypeNode.isNoopTypeCheck()) return vmList;
for (var elem : vmList) {
TruffleSafepoint.poll(this);
elementTypeNode.executeEagerly(frame, elem);
}
@@ -1365,6 +1370,7 @@ public abstract class TypeNode extends PklNode {
var idx = 0;
for (var elem : vmList) {
TruffleSafepoint.poll(this);
var result = elementTypeNode.executeLazily(frame, elem);
if (result != elem) {
ret = ret.replace(idx, result);
@@ -1420,6 +1426,7 @@ public abstract class TypeNode extends PklNode {
protected Object eval(VirtualFrame frame, VmSet value) {
if (elementTypeNode.isNoopTypeCheck()) return value;
for (var elem : value) {
TruffleSafepoint.poll(this);
// no point doing a lazy check because set members have their hash code computed, which
// necessarily deep-forces them.
elementTypeNode.executeEagerly(frame, elem);
@@ -1454,6 +1461,7 @@ public abstract class TypeNode extends PklNode {
@Override
protected Object executeLazily(VirtualFrame frame, Object value) {
if (value instanceof VmMap vmMap) {
TruffleSafepoint.poll(this);
return eval(frame, vmMap);
}
throw typeMismatch(value, BaseModule.getMapClass());
@@ -1462,6 +1470,7 @@ public abstract class TypeNode extends PklNode {
@Override
public Object executeEagerly(VirtualFrame frame, Object value) {
if (value instanceof VmMap vmMap) {
TruffleSafepoint.poll(this);
return evalEager(frame, vmMap);
}
throw typeMismatch(value, BaseModule.getMapClass());
@@ -1801,6 +1810,7 @@ public abstract class TypeNode extends PklNode {
for (var owner = object; owner != null; owner = owner.getParent()) {
var cursor = EconomicMaps.getEntries(owner.getMembers());
while (cursor.advance()) {
TruffleSafepoint.poll(this);
loopCount += 1;
var member = cursor.getValue();
if (member.isProp()) continue;
@@ -20,6 +20,7 @@ import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.TruffleSafepoint;
import com.oracle.truffle.api.TruffleStackTrace;
import com.oracle.truffle.api.dsl.Idempotent;
import com.oracle.truffle.api.frame.*;
@@ -86,8 +87,9 @@ public final class VmUtils {
public static final int REGEX_FLAGS = Pattern.UNICODE_CHARACTER_CLASS | Pattern.UNICODE_CASE;
private static final Engine PKL_ENGINE =
Engine.newBuilder("pkl").option("engine.WarnInterpreterOnly", "false").build();
// must be a singleton because stdlib modules are singletons; otherwise we get "invalid sharing of
// AST Nodes"
private static final Engine PKL_ENGINE = makePklEngine();
@SuppressWarnings("MagicConstant")
private static final Pattern DOC_COMMENT_LINE_START =
@@ -110,6 +112,18 @@ public final class VmUtils {
private VmUtils() {}
public static Engine getEngine() {
return PKL_ENGINE;
}
private static Engine makePklEngine() {
return Engine.newBuilder("pkl").option("engine.WarnInterpreterOnly", "false").build();
}
public static void closeEngine() {
PKL_ENGINE.close(true);
}
static VmTyped createEmptyModule() {
return new VmTyped(createEmptyMaterializedFrame(), null, EconomicMaps.create());
}
@@ -680,17 +694,18 @@ public final class VmUtils {
}
public static List<VmTyped> evaluateAnnotations(
VirtualFrame frame, ExpressionNode[] annotationNodes) {
VirtualFrame frame, Node node, ExpressionNode[] annotationNodes) {
var result = new ArrayList<VmTyped>(annotationNodes.length);
evaluateAnnotations(frame, annotationNodes, result);
evaluateAnnotations(frame, node, annotationNodes, result);
return result;
}
@ExplodeLoop
public static void evaluateAnnotations(
VirtualFrame frame, ExpressionNode[] annotationNodes, List<VmTyped> result) {
VirtualFrame frame, Node node, ExpressionNode[] annotationNodes, List<VmTyped> result) {
for (var annotationNode : annotationNodes) {
TruffleSafepoint.poll(node);
var annotation = (VmTyped) annotationNode.executeGeneric(frame);
// do not force annotations here because running other code
// during class resolution will likely cause evaluation cycles
@@ -34,6 +34,7 @@ import org.gradle.api.tasks.OutputDirectories;
import org.gradle.api.tasks.OutputFiles;
import org.pkl.cli.CliEvaluator;
import org.pkl.cli.CliEvaluatorOptions;
import org.pkl.core.ProfilerOptions;
@CacheableTask
public abstract class EvalTask extends ModulesTask {
@@ -66,7 +67,8 @@ public abstract class EvalTask extends ModulesTask {
getOutputFormat().get(),
getModuleOutputSeparator().get(),
mapAndGetOrNull(getMultipleFileOutputDir(), it -> it.getAsFile().getAbsolutePath()),
getExpression().getOrNull()));
getExpression().getOrNull(),
ProfilerOptions.DEFAULT));
}
@SuppressWarnings("unused")
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id("pklAllProjects")
id("pklJavaLibrary")
alias(libs.plugins.protobuf)
idea
}
dependencies {
annotationProcessor(libs.truffleDslProcessor)
implementation(projects.pklCore)
implementation(libs.graalSdk)
implementation(libs.truffleApi)
implementation(libs.protobuf)
implementation(libs.jspecify)
implementation(libs.truffleProfiler)
}
// prevent `profile.proto` from being added to the classpath; doesn't provide any value
// (implicitly added by the protobuf plugin)
sourceSets.main { resources { exclude("**/*.proto") } }
idea.module { generatedSourceDirs.add(file("build/generated/sources/proto/main/java")) }
protobuf.protoc { artifact = libs.protoc.get().toString() }
@@ -0,0 +1,27 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.profiler;
import java.io.Closeable;
import org.pkl.core.ProfilerOptions;
public interface CpuProfiler extends Closeable {
/** Begin sampling. Throws if already started. */
void start(ProfilerOptions.Cpu options);
/** Stop sampling and write pprof output to the configured file. Idempotent. */
void stop();
}
@@ -0,0 +1,80 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.profiler;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.instrumentation.TruffleInstrument.Registration;
import com.oracle.truffle.tools.profiler.CPUSampler;
import com.oracle.truffle.tools.profiler.impl.CPUSamplerInstrument;
import com.oracle.truffle.tools.profiler.impl.ProfilerToolFactory;
import org.jspecify.annotations.Nullable;
import org.pkl.profiler.impl.CpuProfilerImpl;
/**
* A profiler for Pkl that samples the CPU and generates pprof output.
*
* <p>This instrument doesn't take any engine options. Pkl's polyglot engine is a singleton (stdlib
* modules are singletons, so using different engines results in errors like "Invalid sharing of AST
* Nodes"), and Truffle instruments are associated with engine instances, so there's no way to
* configure the profiler per run via engine options.
*
* <p>Instead, the instrument exposes a {@link CpuProfiler} service. Look it up from the engine's
* instrument and call {@link CpuProfiler#start} and {@link CpuProfiler#stop} to profile a section
* of code.
*/
@Registration(
id = CpuProfilerInstrument.ID,
name = "Pkl CPU Profiler",
services = CpuProfiler.class)
public final class CpuProfilerInstrument extends TruffleInstrument {
public static final String ID = "pkl-cpu-profiler";
private static final ProfilerToolFactory<CPUSampler> cpuSamplerFactory = getCpuSamplerFactory();
private @Nullable CpuProfilerImpl profiler;
/**
* Returns the factory for {@link CPUSampler}.
*
* <p>For some reason, their {@code createFactory} is a private method, so reflection is needed.
* GraalVM's {@link CPUSamplerInstrument} does the same thing.
*/
@SuppressWarnings("unchecked")
private static ProfilerToolFactory<CPUSampler> getCpuSamplerFactory() {
try {
var createFactoryMethod = CPUSampler.class.getDeclaredMethod("createFactory");
createFactoryMethod.setAccessible(true);
return (ProfilerToolFactory<CPUSampler>) createFactoryMethod.invoke(null);
} catch (Exception ex) {
throw new AssertionError(ex);
}
}
@Override
protected void onCreate(Env env) {
var cpuSampler = cpuSamplerFactory.create(env);
profiler = new CpuProfilerImpl(cpuSampler);
env.registerService(profiler);
}
@Override
protected void onDispose(Env env) {
if (profiler != null) {
profiler.close();
}
}
}
@@ -0,0 +1,71 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.profiler.impl;
import com.oracle.truffle.api.instrumentation.SourceSectionFilter;
import com.oracle.truffle.tools.profiler.CPUSampler;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.jspecify.annotations.Nullable;
import org.pkl.core.ProfilerOptions.Cpu;
import org.pkl.core.runtime.VmLanguage;
import org.pkl.profiler.CpuProfiler;
public class CpuProfilerImpl implements CpuProfiler {
private final CPUSampler cpuSampler;
private volatile long startTimeNanos = -1;
private volatile @Nullable Cpu cpuOptions;
public CpuProfilerImpl(CPUSampler cpuSampler) {
this.cpuSampler = cpuSampler;
}
@Override
public void start(Cpu cpuOptions) {
if (!cpuOptions.isEnabled()) {
return;
}
if (this.cpuOptions != null) {
throw new IllegalStateException("CPU profiler is already running");
}
cpuSampler.setPeriod(cpuOptions.samplePeriod());
cpuSampler.setFilter(SourceSectionFilter.newBuilder().mimeTypeIs(VmLanguage.MIME_TYPE).build());
cpuSampler.setCollecting(true);
startTimeNanos = ChronoUnit.NANOS.between(Instant.EPOCH, Instant.now());
this.cpuOptions = cpuOptions;
}
@Override
public void stop() {
var options = this.cpuOptions;
var startTime = this.startTimeNanos;
if (options == null || startTime == -1) return;
this.cpuOptions = null;
this.startTimeNanos = -1;
cpuSampler.setCollecting(false);
var outputFile = options.outputFile();
assert outputFile != null;
// must read data before `clearData()` is called
var data = new PklProfilerData(startTime, cpuSampler.getPeriod(), cpuSampler.getDataList());
cpuSampler.clearData();
new PProfOutput(data, outputFile).write();
}
@Override
public void close() {
cpuSampler.close();
}
}
@@ -0,0 +1,210 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.profiler.impl;
import com.google.perftools.profiles.ProfileProto;
import com.google.perftools.profiles.ProfileProto.Label;
import com.google.perftools.profiles.ProfileProto.Profile;
import com.google.perftools.profiles.ProfileProto.ValueType;
import com.oracle.truffle.tools.profiler.CPUSampler.Payload;
import com.oracle.truffle.tools.profiler.ProfilerNode;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
public final class PProfOutput extends ProfilerOutput {
private final List<String> stringTable = new ArrayList<>();
private final Map<String, Integer> stringIndices = new HashMap<>();
private final Map<String, Long> functionIdsByLabel = new HashMap<>();
private final Map<String, Long> locationIdsByLabel = new HashMap<>();
private final Profile.Builder profileBuilder = Profile.newBuilder();
private long mappingId = 0; // 0 = not yet built; ids are nonzero per the proto's contract
public PProfOutput(PklProfilerData data, Path outputFile) {
super(data, outputFile);
}
@Override
protected void doWriteOutput(PklProfilerData data, Path outputFile) throws Exception {
var profile = buildProfile(data);
Files.createDirectories(outputFile.getParent());
writeOutputFile(profile, outputFile);
}
private Profile buildProfile(PklProfilerData profilerData) {
internString(""); // string_table[0] must be ""
profileBuilder.addSampleType(
ValueType.newBuilder()
.setType(internString("samples"))
.setUnit(internString("count"))
.build());
profileBuilder.setPeriodType(
ValueType.newBuilder()
.setType(internString("wall"))
.setUnit(internString("milliseconds"))
.build());
profileBuilder.setPeriod(profilerData.samplePeriodMs());
profileBuilder.setTimeNanos(profilerData.startTimeNanos());
for (var data : profilerData.dataList()) {
for (var entry : data.getThreadData().entrySet()) {
// in practice there should only ever be one thread; Pkl is single-threaded.
var threadLabel =
Label.newBuilder()
.setKey(internString("thread"))
.setStr(internString(entry.getKey().getName()))
.build();
// entry.getValue() holds the roots of the call tree for this thread; walk down into
// children to build one sample per stack (not per root).
for (var root : entry.getValue()) {
addSamples(root, new ArrayDeque<>(), threadLabel);
}
}
}
profileBuilder.addAllStringTable(stringTable);
return profileBuilder.build();
}
// `locationIds` is leaf-first (current node is always at the head), matching the order
// required by `Sample.location_id`.
private void addSamples(ProfilerNode<Payload> node, Deque<Long> locationIds, Label threadLabel) {
locationIds.push(buildLocation(node, profileBuilder));
var payload = node.getPayload();
// Split self hits by tier so a stalled-in-the-interpreter hot path is distinguishable from
// one that got compiled.
// tier 0: interpreter
// tier 1+: compiled
for (var tier = 0; tier < payload.getNumberOfTiers(); tier++) {
var selfHitCount = payload.getTierSelfCount(tier);
if (selfHitCount > 0) {
var sample =
ProfileProto.Sample.newBuilder().addLabel(threadLabel).addLabel(buildTierLabel(tier));
for (var locationId : locationIds) {
sample.addLocationId(locationId);
}
sample.addValue(selfHitCount);
profileBuilder.addSample(sample);
}
}
for (var child : node.getChildren()) {
addSamples(child, locationIds, threadLabel);
}
locationIds.pop();
}
private Label buildTierLabel(int tier) {
return Label.newBuilder()
.setKey(internString("tier"))
.setStr(internString(tierLabel(tier)))
.build();
}
private String tierLabel(int tier) {
if (tier == 0) {
return "interpreted";
}
return "compiled-" + tier;
}
private void writeOutputFile(Profile profile, Path outputFile) throws IOException {
try (var outputStream = new GZIPOutputStream(Files.newOutputStream(outputFile))) {
profile.writeTo(outputStream);
}
}
private long buildLocation(ProfilerNode<Payload> frame, Profile.Builder profileBuilder) {
var frameRootName = frame.getRootName();
var isInterpreted = frame.getPayload().getTierSelfCount(0) > 0;
var suffix = isInterpreted ? " (interpreted)" : " (jit)";
var name = frameRootName + suffix;
return locationIdsByLabel.computeIfAbsent(
name,
rootName -> {
var functionId = buildFunctionId(frame, rootName);
var id = locationIdsByLabel.size() + 1L;
var line = ProfileProto.Line.newBuilder().setFunctionId(functionId);
var sourceSection = frame.getSourceSection();
if (sourceSection.isAvailable()) {
line.setLine(sourceSection.getStartLine());
}
profileBuilder.addLocation(
ProfileProto.Location.newBuilder()
.setId(id)
.setMappingId(buildMapping())
.addLine(line.build())
.build());
return id;
});
}
private long buildMapping() {
if (mappingId != 0) {
return mappingId;
}
mappingId = 1;
profileBuilder.addMapping(
ProfileProto.Mapping.newBuilder()
.setId(mappingId)
.setFilename(internString("pkl"))
.setHasFunctions(true)
.setHasFilenames(true)
.setHasLineNumbers(true)
.build());
return mappingId;
}
private long buildFunctionId(ProfilerNode<Payload> frame, String rootName) {
return functionIdsByLabel.computeIfAbsent(
rootName,
name -> {
var id = functionIdsByLabel.size() + 1L;
var nameId = internString(name);
var function =
ProfileProto.Function.newBuilder().setId(id).setName(nameId).setSystemName(nameId);
var sourceSection = frame.getSourceSection();
if (sourceSection.isAvailable()) {
var sourceUri = sourceSection.getSource().getURI();
if (sourceUri != null
&& sourceUri.getScheme() != null
&& sourceUri.getScheme().equalsIgnoreCase("file")) {
function.setFilename(internString(sourceUri.getPath()));
}
function.setStartLine(sourceSection.getStartLine());
}
profileBuilder.addFunction(function);
return id;
});
}
private long internString(String str) {
var existing = stringIndices.get(str);
if (existing != null) {
return existing;
}
var index = stringTable.size();
stringTable.add(str);
stringIndices.put(str, index);
return index;
}
}
@@ -0,0 +1,22 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.profiler.impl;
import com.oracle.truffle.tools.profiler.CPUSamplerData;
import java.util.List;
public record PklProfilerData(
long startTimeNanos, long samplePeriodMs, List<CPUSamplerData> dataList) {}
@@ -0,0 +1,40 @@
/*
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.profiler.impl;
import java.nio.file.Path;
abstract class ProfilerOutput {
private final PklProfilerData data;
private final Path outputFile;
public ProfilerOutput(PklProfilerData data, Path outputFile) {
this.data = data;
this.outputFile = outputFile;
}
protected abstract void doWriteOutput(PklProfilerData data, Path outputFile) throws Exception;
public final void write() {
try {
doWriteOutput(data, outputFile);
System.err.printf("[pkl-profiler] Wrote profiler output to %s\n", outputFile);
} catch (Exception e) {
System.err.printf(
"[pkl-profiler] WARN: failed to write profile to %s: %s\n", outputFile, e.getMessage());
}
}
}
@@ -0,0 +1,4 @@
@NullMarked
package org.pkl.profiler.impl;
import org.jspecify.annotations.NullMarked;
@@ -0,0 +1,4 @@
@NullMarked
package org.pkl.profiler;
import org.jspecify.annotations.NullMarked;
+233
View File
@@ -0,0 +1,233 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Profile is a common stacktrace profile format.
//
// Measurements represented with this format should follow the
// following conventions:
//
// - Consumers should treat unset optional fields as if they had been
// set with their default value.
//
// - When possible, measurements should be stored in "unsampled" form
// that is most useful to humans. There should be enough
// information present to determine the original sampled values.
//
// - On-disk, the serialized proto must be gzip-compressed.
//
// - The profile is represented as a set of samples, where each sample
// references a sequence of locations, and where each location belongs
// to a mapping.
// - There is a N->1 relationship from sample.location_id entries to
// locations. For every sample.location_id entry there must be a
// unique Location with that id.
// - There is an optional N->1 relationship from locations to
// mappings. For every nonzero Location.mapping_id there must be a
// unique Mapping with that id.
syntax = "proto3";
package perftools.profiles;
option java_package = "com.google.perftools.profiles";
option java_outer_classname = "ProfileProto";
message Profile {
// A description of the samples associated with each Sample.value.
// For a cpu profile this might be:
// [["cpu","nanoseconds"]] or [["wall","seconds"]] or [["syscall","count"]]
// For a heap profile, this might be:
// [["allocations","count"], ["space","bytes"]],
// If one of the values represents the number of events represented
// by the sample, by convention it should be at index 0 and use
// sample_type.unit == "count".
repeated ValueType sample_type = 1;
// The set of samples recorded in this profile.
repeated Sample sample = 2;
// Mapping from address ranges to the image/binary/library mapped
// into that address range. mapping[0] will be the main binary.
repeated Mapping mapping = 3;
// Locations referenced by samples.
repeated Location location = 4;
// Functions referenced by locations.
repeated Function function = 5;
// A common table for strings referenced by various messages.
// string_table[0] must always be "".
repeated string string_table = 6;
// frames with Function.function_name fully matching the following
// regexp will be dropped from the samples, along with their successors.
int64 drop_frames = 7; // Index into string table.
// frames with Function.function_name fully matching the following
// regexp will be kept, even if it matches drop_frames.
int64 keep_frames = 8; // Index into string table.
// The following fields are informational, do not affect
// interpretation of results.
// Time of collection (UTC) represented as nanoseconds past the epoch.
int64 time_nanos = 9;
// Duration of the profile, if a duration makes sense.
int64 duration_nanos = 10;
// The kind of events between sampled occurrences.
// e.g [ "cpu","cycles" ] or [ "heap","bytes" ]
ValueType period_type = 11;
// The number of events between sampled occurrences.
int64 period = 12;
// Free-form text associated with the profile. The text is displayed as is
// to the user by the tools that read profiles (e.g. by pprof). This field
// should not be used to store any machine-readable information, it is only
// for human-friendly content. The profile must stay functional if this field
// is cleaned.
repeated int64 comment = 13; // Indices into string table.
// Index into the string table of the type of the preferred sample
// value. If unset, clients should default to the last sample value.
int64 default_sample_type = 14;
// Documentation link for this profile type. The URL must be absolute,
// e.g., http://pprof.example.com/cpu-profile.html
//
// The URL may be missing if the profile was generated by code that did not
// supply a link.
int64 doc_url = 15; // Index into string table.
}
// ValueType describes the semantics and measurement units of a value.
message ValueType {
int64 type = 1; // Index into string table.
int64 unit = 2; // Index into string table.
}
// Each Sample records values encountered in some program
// context. The program context is typically a stack trace, perhaps
// augmented with auxiliary information like the thread-id, some
// indicator of a higher level request being handled etc.
message Sample {
// The ids recorded here correspond to a Profile.location.id.
// The leaf is at location_id[0].
repeated uint64 location_id = 1;
// The type and unit of each value is defined by the corresponding
// entry in Profile.sample_type. All samples must have the same
// number of values, the same as the length of Profile.sample_type.
// When aggregating multiple samples into a single sample, the
// result has a list of values that is the element-wise sum of the
// lists of the originals.
repeated int64 value = 2;
// label includes additional context for this sample. It can include
// things like a thread id, allocation size, etc.
//
// NOTE: While possible, having multiple values for the same label key is
// strongly discouraged and should never be used. Most tools (e.g. pprof) do
// not have good (or any) support for multi-value labels. And an even more
// discouraged case is having a string label and a numeric label of the same
// name on a sample. Again, possible to express, but should not be used.
repeated Label label = 3;
}
message Label {
// Index into string table. An annotation for a sample (e.g.
// "allocation_size") with an associated value.
// Keys with "pprof::" prefix are reserved for internal use by pprof.
int64 key = 1;
// At most one of the following must be present
int64 str = 2; // Index into string table
int64 num = 3;
// Should only be present when num is present.
// Specifies the units of num.
// Use arbitrary string (for example, "requests") as a custom count unit.
// If no unit is specified, consumer may apply heuristic to deduce the unit.
// Consumers may also interpret units like "bytes" and "kilobytes" as memory
// units and units like "seconds" and "nanoseconds" as time units,
// and apply appropriate unit conversions to these.
int64 num_unit = 4; // Index into string table
}
message Mapping {
// Unique nonzero id for the mapping.
uint64 id = 1;
// Address at which the binary (or DLL) is loaded into memory.
uint64 memory_start = 2;
// The limit of the address range occupied by this mapping.
uint64 memory_limit = 3;
// Offset in the binary that corresponds to the first mapped address.
uint64 file_offset = 4;
// The object this entry is loaded from. This can be a filename on
// disk for the main binary and shared libraries, or virtual
// abstractions like "[vdso]".
int64 filename = 5; // Index into string table
// A string that uniquely identifies a particular program version
// with high probability. E.g., for binaries generated by GNU tools,
// it could be the contents of the .note.gnu.build-id field.
int64 build_id = 6; // Index into string table
// The following fields indicate the resolution of symbolic info.
bool has_functions = 7;
bool has_filenames = 8;
bool has_line_numbers = 9;
bool has_inline_frames = 10;
}
// Describes function and line table debug information.
message Location {
// Unique nonzero id for the location. A profile could use
// instruction addresses or any integer sequence as ids.
uint64 id = 1;
// The id of the corresponding profile.Mapping for this location.
// It can be unset if the mapping is unknown or not applicable for
// this profile type.
uint64 mapping_id = 2;
// The instruction address for this location, if available. It
// should be within [Mapping.memory_start...Mapping.memory_limit]
// for the corresponding mapping. A non-leaf address may be in the
// middle of a call instruction. It is up to display tools to find
// the beginning of the instruction if necessary.
uint64 address = 3;
// Multiple line indicates this location has inlined functions,
// where the last entry represents the caller into which the
// preceding entries were inlined.
//
// E.g., if memcpy() is inlined into printf:
// line[0].function_name == "memcpy"
// line[1].function_name == "printf"
repeated Line line = 4;
// Provides an indication that multiple symbols map to this location's
// address, for example due to identical code folding by the linker. In that
// case the line information above represents one of the multiple
// symbols. This field must be recomputed when the symbolization state of the
// profile changes.
bool is_folded = 5;
}
message Line {
// The id of the corresponding profile.Function for this line.
uint64 function_id = 1;
// Line number in source code.
int64 line = 2;
// Column number in source code.
int64 column = 3;
}
message Function {
// Unique nonzero id for the function.
uint64 id = 1;
// Name of the function, in human-readable form if available.
int64 name = 2; // Index into string table
// Name of the function, as identified by the system.
// For instance, it can be a C++ mangled name.
int64 system_name = 3; // Index into string table
// Source file containing the function.
int64 filename = 4; // Index into string table
// Line number in source file.
int64 start_line = 5;
}
+2
View File
@@ -57,6 +57,8 @@ include("pkl-server")
include("pkl-tools")
include("pkl-profiler")
include("stdlib")
pluginManagement {