| // Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| // This files contains methods for benchmarking Kernel binary serialization | |
| // and deserialization routines. | |
| import 'dart:io'; | |
| import 'dart:math'; | |
| import 'dart:typed_data'; | |
| import 'package:kernel/ast.dart'; | |
| import 'package:kernel/binary/ast_from_binary.dart'; | |
| import 'package:kernel/binary/ast_to_binary.dart'; | |
| import 'package:kernel/src/printer.dart'; | |
| import '../tool/compile.dart' as compile; | |
| final String usage = | |
| ''' | |
| Usage: kernel_binary_bench.dart [--golem|--raw] {--metadata|--onlyCold} <Benchmark> <SourceDill> | |
| Benchmark can be one of: ${benchmarks.keys.join(', ')} | |
| '''; | |
| typedef void Benchmark(Uint8List bytes); | |
| final Map<String, Benchmark> benchmarks = { | |
| 'AstFromBinaryEager': (Uint8List bytes) { | |
| return _benchmarkAstFromBinary(bytes, eager: true); | |
| }, | |
| 'AstFromBinaryLazy': (Uint8List bytes) { | |
| return _benchmarkAstFromBinary(bytes, eager: false); | |
| }, | |
| 'AstToBinary': (Uint8List bytes) { | |
| return _benchmarkAstToBinary(bytes); | |
| }, | |
| }; | |
| Benchmark? benchmark; | |
| late File sourceDill; | |
| bool forGolem = false; | |
| bool forRaw = false; | |
| bool metadataAware = false; | |
| bool onlyCold = false; | |
| void main(List<String> args) async { | |
| if (args.length == 1 && args[0] == "--compile") { | |
| // Allow - although in practise unused - to go to a bigger target (in this | |
| // case the compile target) - in order to get a more real polymorphic | |
| // potential (especially for AOT compiles). | |
| return await compile.main(args); | |
| } | |
| if (!_parseArgs(args)) { | |
| print(usage); | |
| exit(-1); | |
| } | |
| final Uint8List bytes = sourceDill.readAsBytesSync(); | |
| benchmark!(bytes); | |
| } | |
| int warmupIterations = 100; | |
| int benchmarkIterations = 50; | |
| void _benchmarkAstFromBinary(Uint8List bytes, {bool eager = true}) { | |
| final String nameSuffix = eager ? 'Eager' : 'Lazy'; | |
| final Stopwatch sw = new Stopwatch()..start(); | |
| _fromBinary(bytes, eager: eager); | |
| final int coldRunUs = sw.elapsedMicroseconds; | |
| sw.reset(); | |
| if (onlyCold) { | |
| new BenchmarkResult( | |
| 'AstFromBinary${nameSuffix}', | |
| coldRunUs, | |
| coldRunUs.toDouble(), | |
| [coldRunUs], | |
| ).report(); | |
| return; | |
| } | |
| for (int i = 0; i < warmupIterations; i++) { | |
| _fromBinary(bytes, eager: eager); | |
| } | |
| final double warmupUs = sw.elapsedMicroseconds / warmupIterations; | |
| final List<int> runsUs = new List<int>.filled( | |
| benchmarkIterations, | |
| /* dummy value = */ 0, | |
| ); | |
| for (int i = 0; i < benchmarkIterations; i++) { | |
| sw.reset(); | |
| _fromBinary(bytes, eager: eager, verbose: i == benchmarkIterations - 1); | |
| runsUs[i] = sw.elapsedMicroseconds; | |
| } | |
| new BenchmarkResult( | |
| 'AstFromBinary${nameSuffix}', | |
| coldRunUs, | |
| warmupUs, | |
| runsUs, | |
| ).report(); | |
| } | |
| void _benchmarkAstToBinary(Uint8List bytes) { | |
| final Component p = _fromBinary(bytes, eager: true); | |
| final Stopwatch sw = new Stopwatch()..start(); | |
| _toBinary(p); | |
| final int coldRunUs = sw.elapsedMicroseconds; | |
| sw.reset(); | |
| for (int i = 0; i < warmupIterations; i++) { | |
| _toBinary(p); | |
| } | |
| final double warmupUs = sw.elapsedMicroseconds / warmupIterations; | |
| final List<int> runsUs = new List<int>.filled( | |
| benchmarkIterations, | |
| /* dummy value = */ 0, | |
| ); | |
| for (int i = 0; i < benchmarkIterations; i++) { | |
| sw.reset(); | |
| _toBinary(p); | |
| runsUs[i] = sw.elapsedMicroseconds; | |
| } | |
| new BenchmarkResult('AstToBinary', coldRunUs, warmupUs, runsUs).report(); | |
| } | |
| class BenchmarkResult { | |
| final String name; | |
| final int coldRunUs; | |
| final double warmupUs; | |
| final List<int> runsUs; | |
| new(this.name, this.coldRunUs, this.warmupUs, this.runsUs); | |
| static T add<T extends num>(T x, T y) => x + y as T; | |
| void report() { | |
| runsUs.sort(); | |
| int P(int p) => runsUs[((runsUs.length - 1) * (p / 100)).ceil()]; | |
| final int sum = runsUs.reduce(add); | |
| final double avg = sum / runsUs.length; | |
| final int min = runsUs.first; | |
| final int max = runsUs.last; | |
| final double std = sqrt( | |
| runsUs.map((v) => pow(v - avg, 2)).reduce(add) / runsUs.length, | |
| ); | |
| if (forGolem) { | |
| print('${name}(RunTimeRaw): ${avg} us.'); | |
| print('${name}P50(RunTimeRaw): ${P(50)} us.'); | |
| print('${name}P90(RunTimeRaw): ${P(90)} us.'); | |
| } else if (forRaw) { | |
| runsUs.forEach(print); | |
| } else { | |
| print('${name}Cold: ${coldRunUs} us'); | |
| print('${name}Warmup: ${warmupUs} us'); | |
| print('${name}: ${avg} us.'); | |
| final String prefix = '-' * name.length; | |
| print('${prefix}> Range: ${min}...${max} us.'); | |
| print('${prefix}> Std Dev: ${std.toStringAsFixed(2)}'); | |
| print('${prefix}> 50th percentile: ${P(50)} us.'); | |
| print('${prefix}> 90th percentile: ${P(90)} us.'); | |
| } | |
| } | |
| } | |
| bool _parseArgs(List<String> argsOrg) { | |
| List<String> trimmedArgs = []; | |
| for (String arg in argsOrg) { | |
| if (arg == "--golem") { | |
| forGolem = true; | |
| } else if (arg == "--raw") { | |
| forRaw = true; | |
| } else if (arg == "--metadata") { | |
| metadataAware = true; | |
| } else if (arg == "--onlyCold") { | |
| onlyCold = true; | |
| } else if (arg.startsWith("--warmups=")) { | |
| warmupIterations = int.parse(arg.substring("--warmups=".length)); | |
| } else if (arg.startsWith("--iterations=")) { | |
| benchmarkIterations = int.parse(arg.substring("--iterations=".length)); | |
| } else { | |
| trimmedArgs.add(arg); | |
| } | |
| } | |
| if (trimmedArgs.length != 2) { | |
| return false; | |
| } | |
| if (forGolem && forRaw) { | |
| return false; | |
| } | |
| benchmark = benchmarks[trimmedArgs[0]]; | |
| if (benchmark == null) { | |
| return false; | |
| } | |
| sourceDill = new File(trimmedArgs[1]); | |
| if (!sourceDill.existsSync()) { | |
| return false; | |
| } | |
| return true; | |
| } | |
| Component _fromBinary( | |
| Uint8List bytes, { | |
| required bool eager, | |
| bool verbose = false, | |
| }) { | |
| Component component = new Component(); | |
| if (metadataAware) { | |
| // This is currently (October 2024) what VmTarget.configureComponent does. | |
| component.metadata.putIfAbsent( | |
| CallSiteAttributesMetadataRepository.repositoryTag, | |
| () => new CallSiteAttributesMetadataRepository(), | |
| ); | |
| BinaryBuilderWithMetadata builder = new BinaryBuilderWithMetadata( | |
| bytes, | |
| filename: 'filename', | |
| disableLazyReading: eager, | |
| ); | |
| builder.readComponent(component); | |
| if (verbose) { | |
| // No current verbose output. | |
| } | |
| } else { | |
| new BinaryBuilder( | |
| bytes, | |
| filename: 'filename', | |
| disableLazyReading: eager, | |
| ).readComponent(component); | |
| } | |
| return component; | |
| } | |
| class SimpleSink implements Sink<List<int>> { | |
| final List<List<int>> chunks = <List<int>>[]; | |
| @override | |
| void add(List<int> chunk) { | |
| chunks.add(chunk); | |
| } | |
| @override | |
| void close() {} | |
| } | |
| void _toBinary(Component p) { | |
| new BinaryPrinter(new SimpleSink()).writeComponentFile(p); | |
| } | |
| // The below is copied from package:vm so to test metadata properly without | |
| // depending on package:vm. | |
| /// Metadata for annotating call sites with various attributes. | |
| class CallSiteAttributesMetadata { | |
| final DartType receiverType; | |
| const new({required this.receiverType}); | |
| @override | |
| String toString() => | |
| "receiverType:${receiverType.toText(astTextStrategyForTesting)}"; | |
| } | |
| /// Repository for [CallSiteAttributesMetadata]. | |
| class CallSiteAttributesMetadataRepository | |
| extends MetadataRepository<CallSiteAttributesMetadata> { | |
| static final repositoryTag = 'vm.call-site-attributes.metadata'; | |
| @override | |
| final String tag = repositoryTag; | |
| @override | |
| final Map<TreeNode, CallSiteAttributesMetadata> mapping = | |
| <TreeNode, CallSiteAttributesMetadata>{}; | |
| @override | |
| void writeToBinary( | |
| CallSiteAttributesMetadata metadata, | |
| Node node, | |
| BinarySink sink, | |
| ) { | |
| sink.writeDartType(metadata.receiverType); | |
| } | |
| @override | |
| CallSiteAttributesMetadata readFromBinary(Node node, BinarySource source) { | |
| final type = source.readDartType(); | |
| return new CallSiteAttributesMetadata(receiverType: type); | |
| } | |
| } |