Files
pkl/stdlib/Benchmark.pkl
2025-11-03 12:26:58 -08:00

233 lines
7.1 KiB
Plaintext

//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 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.
//===----------------------------------------------------------------------===//
/// A template for writing and running benchmarks.
///
/// To write benchmarks, amend this module and define [microbenchmarks],
/// [outputBenchmarks], and/or [parserBenchmarks].
/// To run benchmarks, evaluate the amended module.
///
/// Each benchmark run consists of *m* [iterations] of *n* repetitions.
/// The number of repetitions is controlled indirectly via [iterationTime].
///
/// The reported metric is time per repetition.
/// It is calculated as measured iteration time divided by number of repetitions.
///
/// Benchmarks are warmed up for approximately the same time they are measured.
/// The goal is to measure steady state performance.
///
/// The benchmark [report] details benchmark [results][BenchmarkResult]
/// and the [platform][_platform.Platform] that benchmarks were run on.
/// By default, the report is rendered in *Pcf*.
/// To render the report in a different format, override `output.renderer`.
///
/// Warning: Although this module is ready for initial use,
/// benchmark results may be inaccurate or inconsistent.
@ModuleInfo { minPklVersion = "0.31.0" }
module pkl.Benchmark
import "pkl:platform" as _platform
/// The number of benchmark iterations to run.
///
/// This value can be [overridden][Benchmark.iterations] per benchmark.
iterations: UInt32 = 15
/// The approximate time to spend on each benchmark iteration.
///
/// This value indirectly controls the number of repetitions per iteration.
///
/// This value can be [overridden][Benchmark.iterationTime] per benchmark.
iterationTime: Duration = 100.ms
/// Whether to report detailed results such as [BenchmarkResult.samples].
///
/// This value can be [overridden][Benchmark.isVerbose] per benchmark.
isVerbose: Boolean = false
/// Benchmarks that measure the time taken to evaluate an expression.
///
/// Mapping keys are the benchmarks' descriptive names.
microbenchmarks: Mapping<String, Microbenchmark> = new {
default {
iterations = module.iterations
iterationTime = module.iterationTime
isVerbose = module.isVerbose
}
}
/// Benchmarks that measure the time taken to render a module's output.
///
/// Mapping keys are the benchmarks' descriptive names.
outputBenchmarks: Mapping<String, OutputBenchmark> = new {
default {
iterations = module.iterations
iterationTime = module.iterationTime
isVerbose = module.isVerbose
}
}
/// Benchmarks that measure the time taken to parse a module's source code into an abstract syntax tree.
///
/// Mapping keys are the benchmarks' descriptive names.
parserBenchmarks: Mapping<String, ParserBenchmark> = new {
default {
iterations = module.iterations
iterationTime = module.iterationTime
isVerbose = module.isVerbose
}
}
/// The report containing benchmark results. Typically not configured from user code.
report: BenchmarkReport = new {
platform = _platform.current
when (!module.microbenchmarks.isEmpty) {
microbenchmarks {
for (name, benchmark in module.microbenchmarks) {
[name] = benchmark.run()
}
}
}
when (!module.outputBenchmarks.isEmpty) {
outputBenchmarks {
for (name, benchmark in module.outputBenchmarks) {
[name] = benchmark.run()
}
}
}
when (!module.parserBenchmarks.isEmpty) {
parserBenchmarks {
for (name, benchmark in module.parserBenchmarks) {
[name] = benchmark.run()
}
}
}
}
/// Common base class for benchmarks.
abstract class Benchmark {
/// The number of benchmark iterations to run.
iterations: UInt32
/// The approximate time to spend on each benchmark iteration.
///
/// This value indirectly controls the number of repetitions per iteration.
iterationTime: Duration
/// Whether to report detailed results such as [BenchmarkResult.samples].
isVerbose: Boolean
/// Runs this benchmark. Typically not called from user code.
abstract function run(): BenchmarkResult
}
/// The result of running a benchmark.
class BenchmarkResult {
/// The number of iterations run.
iterations: UInt32
/// The number of repetitions run per iteration.
repetitions: UInt
/// The average time that a repetition took to complete in each iteration,
/// calculated as measured iteration time divided by number of repetitions.
///
/// This property is only set if [Benchmark.isVerbose] is [true].
samples: List<Duration>(length == iterations)?
/// The minimum of [samples].
min: Duration
/// The maximum of [samples].
max: Duration
/// The mean of [samples].
mean: Duration
/// The standard deviation of [samples].
stdev: Duration
/// The margin of error for [mean] at 99% confidence level.
error: Duration
}
/// A benchmark that measures the time taken to evaluate an expression.
class Microbenchmark extends Benchmark {
/// The expression to evaluate.
expression: Any
external function run(): BenchmarkResult
}
/// A benchmark that measures the time taken to render a module's output.
class OutputBenchmark extends Benchmark {
/// The module to evaluate.
sourceModule: Module
external function run(): BenchmarkResult
}
/// A benchmark that measures the time taken to parse a module's source code.
class ParserBenchmark extends Benchmark {
/// The module source code to parse.
@SourceCode { language = "Pkl" }
sourceText: String
/// The effective URI of [sourceText].
///
/// If [sourceText] contains relative imports, set [sourceUri] so that these imports may be
/// resolved.
sourceUri: String = "repl:text"
external function run(): BenchmarkResult
}
/// The results of running a module's benchmarks.
class BenchmarkReport {
/// The results of the microbenchmarks that were run.
///
/// Mapping keys are the benchmarks' descriptive names.
microbenchmarks: Mapping<String, BenchmarkResult>?
/// The results of the output benchmarks that were run.
///
/// Mapping keys are the benchmarks' descriptive names.
outputBenchmarks: Mapping<String, BenchmarkResult>?
/// The results of the parser benchmarks that were run.
///
/// Mapping keys are the benchmarks' descriptive names.
parserBenchmarks: Mapping<String, BenchmarkResult>?
/// The platform that benchmarks were run on.
platform: _platform.Platform
}
output {
value = report
renderer = new PcfRenderer {
omitNullProperties = true
converters {
// round durations to two decimal places
[Duration] = (it: Duration) -> it.value.toFixed(2).toFloat().toDuration(it.unit)
}
}
}