From f96fba00e18a22ba7b0d890df9bb88be612750f1 Mon Sep 17 00:00:00 2001 From: Daniel Chao Date: Thu, 15 Oct 2026 08:57:55 -0700 Subject: [PATCH] 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 Co-authored-by: Islon Scherer --- THIRD-PARTY-NOTICES.txt | 53 ++++ .../src/main/kotlin/NativeImageBuild.kt | 2 + .../src/main/kotlin/pklFatJar.gradle.kts | 5 + docs/modules/pkl-cli/pages/index.adoc | 18 ++ gradle/libs.versions.toml | 8 +- pkl-cli/pkl-cli.gradle.kts | 88 +++++++ .../kotlin/org/pkl/cli/CliCommandRunner.kt | 10 +- .../main/kotlin/org/pkl/cli/CliEvaluator.kt | 14 +- .../kotlin/org/pkl/cli/CliEvaluatorOptions.kt | 8 + .../main/kotlin/org/pkl/cli/CliTestRunner.kt | 10 +- .../org/pkl/cli/commands/EvalCommand.kt | 20 +- .../kotlin/org/pkl/cli/commands/RunCommand.kt | 7 +- .../org/pkl/cli/commands/TestCommand.kt | 17 +- pkl-cli/src/main/kotlin/org/pkl/cli/utils.kt | 38 +++ .../kotlin/org/pkl/cli/ProfilerOutputTest.kt | 199 +++++++++++++++ .../pkl/cli/SubprocessProfilerOutputTest.kt | 131 ++++++++++ .../commons/cli/commands/ProfileOptions.kt | 49 ++++ .../java/org/pkl/core/ProfilerOptions.java | 31 +++ .../generator/GeneratorForNode.java | 2 + .../generator/GeneratorObjectLiteralNode.java | 2 + .../expression/literal/AmendModuleNode.java | 2 +- .../expression/literal/BytesLiteralNode.java | 2 + .../literal/ElementsEntriesLiteralNode.java | 1 + .../literal/EntriesLiteralNode.java | 2 + .../InterpolatedStringLiteralNode.java | 4 +- .../expression/literal/ListLiteralNode.java | 4 +- .../expression/literal/MapLiteralNode.java | 4 +- .../expression/literal/SetLiteralNode.java | 4 +- .../literal/SpecializedObjectLiteralNode.java | 2 + .../member/AbstractInvokeMethodNode.java | 2 + .../org/pkl/core/ast/member/ClassNode.java | 2 +- .../pkl/core/ast/member/TypeAliasNode.java | 2 +- .../core/ast/member/UnresolvedMethodNode.java | 2 +- .../ast/member/UnresolvedPropertyNode.java | 2 +- .../java/org/pkl/core/ast/type/TypeNode.java | 10 + .../java/org/pkl/core/runtime/VmUtils.java | 25 +- .../java/org/pkl/gradle/task/EvalTask.java | 4 +- pkl-profiler/pkl-profiler.gradle.kts | 39 +++ .../java/org/pkl/profiler/CpuProfiler.java | 27 ++ .../pkl/profiler/CpuProfilerInstrument.java | 80 ++++++ .../pkl/profiler/impl/CpuProfilerImpl.java | 71 ++++++ .../org/pkl/profiler/impl/PProfOutput.java | 210 ++++++++++++++++ .../pkl/profiler/impl/PklProfilerData.java | 22 ++ .../org/pkl/profiler/impl/ProfilerOutput.java | 40 +++ .../org/pkl/profiler/impl/package-info.java | 4 + .../java/org/pkl/profiler/package-info.java | 4 + pkl-profiler/src/main/proto/profile.proto | 233 ++++++++++++++++++ settings.gradle.kts | 2 + 48 files changed, 1480 insertions(+), 38 deletions(-) create mode 100644 pkl-cli/src/main/kotlin/org/pkl/cli/utils.kt create mode 100644 pkl-cli/src/test/kotlin/org/pkl/cli/ProfilerOutputTest.kt create mode 100644 pkl-cli/src/test/kotlin/org/pkl/cli/SubprocessProfilerOutputTest.kt create mode 100644 pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ProfileOptions.kt create mode 100644 pkl-core/src/main/java/org/pkl/core/ProfilerOptions.java create mode 100644 pkl-profiler/pkl-profiler.gradle.kts create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/CpuProfiler.java create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/CpuProfilerInstrument.java create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/impl/CpuProfilerImpl.java create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/impl/PProfOutput.java create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/impl/PklProfilerData.java create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/impl/ProfilerOutput.java create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/impl/package-info.java create mode 100644 pkl-profiler/src/main/java/org/pkl/profiler/package-info.java create mode 100644 pkl-profiler/src/main/proto/profile.proto diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index a083a15df..27350c144 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -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. diff --git a/build-logic/src/main/kotlin/NativeImageBuild.kt b/build-logic/src/main/kotlin/NativeImageBuild.kt index 6821c4a9a..7708c8073 100644 --- a/build-logic/src/main/kotlin/NativeImageBuild.kt +++ b/build-logic/src/main/kotlin/NativeImageBuild.kt @@ -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/.*") diff --git a/build-logic/src/main/kotlin/pklFatJar.gradle.kts b/build-logic/src/main/kotlin/pklFatJar.gradle.kts index 9c1815b96..e07b216ef 100644 --- a/build-logic/src/main/kotlin/pklFatJar.gradle.kts +++ b/build-logic/src/main/kotlin/pklFatJar.gradle.kts @@ -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) { diff --git a/docs/modules/pkl-cli/pages/index.adoc b/docs/modules/pkl-cli/pages/index.adoc index a6e5ad5b2..00cc2bb26 100644 --- a/docs/modules/pkl-cli/pages/index.adoc +++ b/docs/modules/pkl-cli/pages/index.adoc @@ -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 <>. [[command-server]] diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8393a8e9d..6804d1e93 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/pkl-cli/pkl-cli.gradle.kts b/pkl-cli/pkl-cli.gradle.kts index 79f5d9422..47dbe6430 100644 --- a/pkl-cli/pkl-cli.gradle.kts +++ b/pkl-cli/pkl-cli.gradle.kts @@ -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("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("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("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().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 { diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliCommandRunner.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliCommandRunner.kt index f23a43789..f8b030795 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliCommandRunner.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliCommandRunner.kt @@ -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, 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() + } } } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt index 4905e8a48..bac1547e4 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluator.kt @@ -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() + } } } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt index 4b1c13a7e..9219ab7f1 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliEvaluatorOptions.kt @@ -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 { diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt index d3418e4de..e3f812374 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliTestRunner.kt @@ -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() + } } } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt index 14dc6fa79..112718c32 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/EvalCommand.kt @@ -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() } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/RunCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/RunCommand.kt index baa0d37bb..8f0406eb5 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/RunCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/RunCommand.kt @@ -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() } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt index 5fa21d0f9..c0091301f 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/commands/TestCommand.kt @@ -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() } diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/utils.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/utils.kt new file mode 100644 index 000000000..2adb02e71 --- /dev/null +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/utils.kt @@ -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 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() + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/ProfilerOutputTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/ProfilerOutputTest.kt new file mode 100644 index 000000000..3f8693cac --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/ProfilerOutputTest.kt @@ -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() + } +} diff --git a/pkl-cli/src/test/kotlin/org/pkl/cli/SubprocessProfilerOutputTest.kt b/pkl-cli/src/test/kotlin/org/pkl/cli/SubprocessProfilerOutputTest.kt new file mode 100644 index 000000000..cfa0831e7 --- /dev/null +++ b/pkl-cli/src/test/kotlin/org/pkl/cli/SubprocessProfilerOutputTest.kt @@ -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=` (or `-Dorg.pkl.cli.testExecutable=`). + */ +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 { + 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) { + 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")) + } +} diff --git a/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ProfileOptions.kt b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ProfileOptions.kt new file mode 100644 index 000000000..0e7aa5e85 --- /dev/null +++ b/pkl-commons-cli/src/main/kotlin/org/pkl/commons/cli/commands/ProfileOptions.kt @@ -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) + ) +} diff --git a/pkl-core/src/main/java/org/pkl/core/ProfilerOptions.java b/pkl-core/src/main/java/org/pkl/core/ProfilerOptions.java new file mode 100644 index 000000000..a0f251391 --- /dev/null +++ b/pkl-core/src/main/java/org/pkl/core/ProfilerOptions.java @@ -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; + } + } +} diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java index bc2bc2568..07ea357c9 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorForNode.java @@ -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 = diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorObjectLiteralNode.java index 18767d30b..f527ea850 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorObjectLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/generator/GeneratorObjectLiteralNode.java @@ -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; diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java index 98c0aec69..3bf02cef3 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/AmendModuleNode.java @@ -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; } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/BytesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/BytesLiteralNode.java index 8aa78ff08..5e7b84b73 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/BytesLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/BytesLiteralNode.java @@ -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) { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java index 9e9893429..32f33d4c1 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ElementsEntriesLiteralNode.java @@ -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]); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java index 4ff4fb9bf..5615c9508 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/EntriesLiteralNode.java @@ -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); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/InterpolatedStringLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/InterpolatedStringLiteralNode.java index c1e098fcf..dc9368522 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/InterpolatedStringLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/InterpolatedStringLiteralNode.java @@ -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); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ListLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ListLiteralNode.java index 69234eb1b..c4007982d 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ListLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/ListLiteralNode.java @@ -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)); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/MapLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/MapLiteralNode.java index 245258a83..e8c21e627 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/MapLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/MapLiteralNode.java @@ -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); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SetLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SetLiteralNode.java index 153e1ec7a..f3b514c41 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SetLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SetLiteralNode.java @@ -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)); } diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java index 6d2c0afbd..23e83fba4 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/literal/SpecializedObjectLiteralNode.java @@ -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(); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/AbstractInvokeMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/AbstractInvokeMethodNode.java index a8bf0a82f..cabfbf9ca 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/expression/member/AbstractInvokeMethodNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/expression/member/AbstractInvokeMethodNode.java @@ -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 { diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java index 258d08cca..c05d1afe4 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/ClassNode.java @@ -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)); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/TypeAliasNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeAliasNode.java index 1fdf81c12..394389db0 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/TypeAliasNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/TypeAliasNode.java @@ -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); diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedMethodNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedMethodNode.java index 9ee989a73..7ffea73f9 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedMethodNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedMethodNode.java @@ -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 = diff --git a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedPropertyNode.java b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedPropertyNode.java index 368edf71a..5698bb576 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedPropertyNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/member/UnresolvedPropertyNode.java @@ -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 diff --git a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java index 29dd7df34..308de04e1 100644 --- a/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java +++ b/pkl-core/src/main/java/org/pkl/core/ast/type/TypeNode.java @@ -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; diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java index c999c80e0..bace0c398 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/VmUtils.java @@ -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 evaluateAnnotations( - VirtualFrame frame, ExpressionNode[] annotationNodes) { + VirtualFrame frame, Node node, ExpressionNode[] annotationNodes) { var result = new ArrayList(annotationNodes.length); - evaluateAnnotations(frame, annotationNodes, result); + evaluateAnnotations(frame, node, annotationNodes, result); return result; } @ExplodeLoop public static void evaluateAnnotations( - VirtualFrame frame, ExpressionNode[] annotationNodes, List result) { + VirtualFrame frame, Node node, ExpressionNode[] annotationNodes, List 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 diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java index 7f486f711..04a1855c8 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/EvalTask.java @@ -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") diff --git a/pkl-profiler/pkl-profiler.gradle.kts b/pkl-profiler/pkl-profiler.gradle.kts new file mode 100644 index 000000000..bc1feb8a6 --- /dev/null +++ b/pkl-profiler/pkl-profiler.gradle.kts @@ -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() } diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/CpuProfiler.java b/pkl-profiler/src/main/java/org/pkl/profiler/CpuProfiler.java new file mode 100644 index 000000000..78b171767 --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/CpuProfiler.java @@ -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(); +} diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/CpuProfilerInstrument.java b/pkl-profiler/src/main/java/org/pkl/profiler/CpuProfilerInstrument.java new file mode 100644 index 000000000..e4362ce5f --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/CpuProfilerInstrument.java @@ -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. + * + *

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. + * + *

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 cpuSamplerFactory = getCpuSamplerFactory(); + + private @Nullable CpuProfilerImpl profiler; + + /** + * Returns the factory for {@link CPUSampler}. + * + *

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 getCpuSamplerFactory() { + try { + var createFactoryMethod = CPUSampler.class.getDeclaredMethod("createFactory"); + createFactoryMethod.setAccessible(true); + return (ProfilerToolFactory) 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(); + } + } +} diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/impl/CpuProfilerImpl.java b/pkl-profiler/src/main/java/org/pkl/profiler/impl/CpuProfilerImpl.java new file mode 100644 index 000000000..f0694ea91 --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/impl/CpuProfilerImpl.java @@ -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(); + } +} diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/impl/PProfOutput.java b/pkl-profiler/src/main/java/org/pkl/profiler/impl/PProfOutput.java new file mode 100644 index 000000000..5b166647a --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/impl/PProfOutput.java @@ -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 stringTable = new ArrayList<>(); + private final Map stringIndices = new HashMap<>(); + private final Map functionIdsByLabel = new HashMap<>(); + private final Map 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 node, Deque 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 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 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; + } +} diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/impl/PklProfilerData.java b/pkl-profiler/src/main/java/org/pkl/profiler/impl/PklProfilerData.java new file mode 100644 index 000000000..5e9d79df0 --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/impl/PklProfilerData.java @@ -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 dataList) {} diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/impl/ProfilerOutput.java b/pkl-profiler/src/main/java/org/pkl/profiler/impl/ProfilerOutput.java new file mode 100644 index 000000000..42c816b1e --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/impl/ProfilerOutput.java @@ -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()); + } + } +} diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/impl/package-info.java b/pkl-profiler/src/main/java/org/pkl/profiler/impl/package-info.java new file mode 100644 index 000000000..0a529050b --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/impl/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.pkl.profiler.impl; + +import org.jspecify.annotations.NullMarked; diff --git a/pkl-profiler/src/main/java/org/pkl/profiler/package-info.java b/pkl-profiler/src/main/java/org/pkl/profiler/package-info.java new file mode 100644 index 000000000..cad7b915f --- /dev/null +++ b/pkl-profiler/src/main/java/org/pkl/profiler/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.pkl.profiler; + +import org.jspecify.annotations.NullMarked; diff --git a/pkl-profiler/src/main/proto/profile.proto b/pkl-profiler/src/main/proto/profile.proto new file mode 100644 index 000000000..128b546b7 --- /dev/null +++ b/pkl-profiler/src/main/proto/profile.proto @@ -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; +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b0c6ee3dd..bbece6fc3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,6 +57,8 @@ include("pkl-server") include("pkl-tools") +include("pkl-profiler") + include("stdlib") pluginManagement {