diff --git a/runtime/tools/profiling/.gitignore b/runtime/tools/profiling/.gitignore new file mode 100644 index 00000000000..3a857904084 --- /dev/null +++ b/runtime/tools/profiling/.gitignore @@ -0,0 +1,3 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ diff --git a/runtime/tools/profiling/CHANGELOG.md b/runtime/tools/profiling/CHANGELOG.md new file mode 100644 index 00000000000..a0712a79e75 --- /dev/null +++ b/runtime/tools/profiling/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +- Initial version. diff --git a/runtime/tools/profiling/README.md b/runtime/tools/profiling/README.md new file mode 100644 index 00000000000..53a31e63cce --- /dev/null +++ b/runtime/tools/profiling/README.md @@ -0,0 +1,64 @@ +Various tools for low level profing of code running on the Dart VM. + +# Uprobe based profiling + +[uprobes](https://www.kernel.org/doc/html/latest/trace/uprobetracer.html) is +a user-space dynamic tracing mechanism. Using this mechanism the kernel can +be instructed to place a tracepoint at a particular file offset within a +specific binary. Whenever this tracepoint is hit the kernel will fetch values +from the execution context based on the uprobe's description and emit an event. +A developer can subscribe to uprobe events in a few different ways including +[perf_event_open](https://man7.org/linux/man-pages/man2/perf_event_open.2.html) +syscall. uprobes have been enabled by default on all newish Linux kernels +(4.14+), however they are only truly usable on Android/ARM64 starting from +5.10+. `bin/set_uprobe.dart` is a helper script for placing uprobes inside +binaries and using this for profiling. + +The core workflow looks like this: + +```console +$ sudo $(which dart) runtime/tools/profiling/bin/set_uprobe.dart probeName symbol binary +``` + +This will create an uprobe with name `probeName` which triggers whenever +the given `symbol` inside the given `binary` is called. You can then record +an event (and collect the call stack) using: + +```console +$ sudo perf record -g -e uprobes:probeName ... +``` + +## Allocation profiling with uprobes + +AOT compiler can emit a special probe point (`stub AllocationProbePoint`) which +triggers for each new space allocation from generated code. `set_uprobe` script +has special support for this probe point: it will configure probe point to +record additional information (address of allocated object, allocation top and +cid of the allocated object) allowing to post process collected data into +an actual allocation profile. + +Start by compiling your application with `--generate-probe-points`: + +```console +$ pkg/vm/tool/precompiler2 --generate-probe-points test.dart test.aot +``` + +Then install uprobe on `AllocationProbePoint`: + +```console +$ sudo $(which dart) runtime/tools/profiling/bin/set_uprobe.dart alloc AllocationProbePoint test.aot +``` + +Record the profile: + +``` +$ sudo perf record -g -e uprobes:alloc out/ReleaseX64/dart_precompiled_runtime test.aot +$ sudo chmod 0755 perf.data +``` + +Produce a coalesced allocation profile from the recording: + +``` +$ dart runtime/tools/profiling/bin/convert_allocation_profile.dart perf.data +$ pprof -flame pprof.profile +``` \ No newline at end of file diff --git a/runtime/tools/profiling/analysis_options.yaml b/runtime/tools/profiling/analysis_options.yaml new file mode 100644 index 00000000000..dee8927aafe --- /dev/null +++ b/runtime/tools/profiling/analysis_options.yaml @@ -0,0 +1,30 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +# Uncomment the following section to specify additional rules. + +# linter: +# rules: +# - camel_case_types + +# analyzer: +# exclude: +# - path/to/excluded/files/** + +# For more information about the core and recommended set of lints, see +# https://dart.dev/go/core-lints + +# For additional information about configuring this file, see +# https://dart.dev/guides/language/analysis-options diff --git a/runtime/tools/profiling/bin/convert_allocation_profile.dart b/runtime/tools/profiling/bin/convert_allocation_profile.dart new file mode 100644 index 00000000000..11ef14944e8 --- /dev/null +++ b/runtime/tools/profiling/bin/convert_allocation_profile.dart @@ -0,0 +1,537 @@ +// 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. + +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ffi'; + +import 'package:fixnum/fixnum.dart' hide Int32; + +import 'package:profiling/src/perf/perf_data.dart'; +import 'package:profiling/src/symbols.dart'; +import 'package:profiling/src/pprof/generated/profile.pb.dart' as pprof; + +/// `PERF_RECORD_SAMPLE` with the following optional fields: +/// +/// `PERF_SAMPLE_IP`, `PERF_SAMPLE_TID`, `PERF_SAMPLE_TIME`, `PERF_SAMPLE_CALLCHAIN`, +/// `PERF_SAMPLE_CPU`, `PERF_SAMPLE_PERIOD`, `PERF_SAMPLE_RAW`. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L947 +final class SampleEvent extends Struct { + external EventHeader header; + + /// Enabled by `PERF_SAMPLE_IP` + @Uint64() + external int ip; + + /// Enabled by `PERF_SAMPLE_TID` + @Uint32() + external int pid; + + /// Enabled by `PERF_SAMPLE_TID` + @Uint32() + external int tid; + + /// Enabled by `PERF_SAMPLE_TIME` + @Uint64() + external int time; + + /// Enabled by `PERF_SAMPLE_CPU` + @Uint32() + external int cpu; + + /// Enabled by `PERF_SAMPLE_CPU` + @Uint32() + external int res; + + /// Enabled by `PERF_SAMPLE_PERIOD` + @Uint64() + external int period; + + /// Enabled by `PERF_SAMPLE_CALLCHAIN` + @Uint64() + external int nr; + + /// Enabled by `PERF_SAMPLE_CALLCHAIN` + @Array.variable() + external Array ips; +} + +/// Data recorded by the probe stored in a `PERF_SAMPLE_RAW`. +/// +/// The `size` field is part of `PERF_SAMPLE_RAW` encoding the rest are +/// part of probe data itself. Format for the recorded data can be recovered +/// by loading tracepoint information from an optional section identified +/// by `HEADER_TRACING_DATA` ([OptionalSection.tracingData]). However encoding +/// of that section is extremely bespoke (see `trace-event-read.c` below), so +/// instead of fiddling with that we simply hardcode expected format of the +/// probe. This obviously needs to be kept in sync with `set_uprobe.dart` +/// script. +/// +/// ``` +/// $ sudo cat /sys/kernel/tracing/events/uprobes/alloc/format +/// name: alloc +/// ID: 1976 +/// format: +/// field:unsigned short common_type; offset:0; size:2; signed:0; +/// field:unsigned char common_flags; offset:2; size:1; signed:0; +/// field:unsigned char common_preempt_count; offset:3; size:1; signed:0; +/// field:int common_pid; offset:4; size:4; signed:1; +/// +/// field:unsigned long __probe_ip; offset:8; size:8; signed:0; +/// field:s64 addr; offset:16; size:8; signed:1; +/// field:s64 top; offset:24; size:8; signed:1; +/// field:u32 cid; offset:32; size:4; signed:0; +/// print fmt: "(%lx) addr=%Ld top=%Ld cid=%u", REC->__probe_ip, REC->addr, REC->top, REC->cid +/// ``` +/// +/// [^1]: https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/trace-event-read.c#L375 +@Packed(1) +final class ProbeData extends Struct { + @Uint32() + external int size; + + @Uint16() + external int commonType; + + @Uint8() + external int commonFlags; + + @Uint8() + external int commonPreemptCount; + + @Int32() + external int commonPid; + + @Uint64() + external int probeIp; + + @Uint64() + external int addr; + + @Uint64() + external int top; + + @Uint32() + external int cid; + + @override + String toString() => + 'Probe{addr=${addr.formatAsAddress()},top=${top.formatAsAddress()},cid=$cid}'; +} + +/// Lazily populated mapping between file offsets in a binary and profile +/// location ids. +/// +/// This class handles convertion of the file offset to the corresponding +/// symbol name and futher into corresponding location id inside the profile. +final class SymbolsIndex { + final ProfileBuilder profileBuilder; + + final Symbols symbols; + final List ids; + + SymbolsIndex(this.profileBuilder, this.symbols) + : ids = List.filled(symbols.fileOffsets.length, null); + + static final lineRe = + RegExp(r"^(?[0-9a-f]+)\s+(?\w+)\s+(?.*)$"); + + /// Return location id corresponding to the given [fileOffset]. + /// + /// This function will lazily allocate new ids as necessary by + /// calling [ProfileBuilder.addSymbol]. + Int64? symbolId(int fileOffset) { + final index = symbols.symbolIndex(fileOffset); + if (index != null) { + return (ids[index] ??= profileBuilder.addSymbol(symbols.names[index])); + } + return null; + } +} + +final class Mapping { + final int baseAddress; + final int length; + final String path; + final int offset; + + Mapping({ + required this.baseAddress, + required this.length, + required this.path, + required this.offset, + }); +} + +/// Symbols information for the whole address space. +final class AddressSpaceSymbols { + /// Base addresses for mapping ranges. + /// + /// To simplify search we also add ranges that don't have any symbols here. + /// Consider for example that we have two mappings `[A, A')` and `[B, B')` + /// with symbols (`Sym(A)` and `Sym(B)` respectively). In this case: + /// * [baseAddresses] will contain `[0, A, A', B, B']` and + /// * [symbolsIndexes] will contain `[null, SA, null, SymB, null]`. + final Int64List baseAddresses; + + /// Symbol indexes corresponding to mappings in [baseAddresses]. + final List symbolsIndexes; + + /// File offsets corresponding to mappings in [baseAddresses]. + final Int64List fileOffsets; + + AddressSpaceSymbols._( + this.baseAddresses, this.symbolsIndexes, this.fileOffsets); + + Int64? symbolId(int address) { + // We use linear search because we assume the number of mappings + // is very small (~2). + final limit = baseAddresses.length - 1; + for (var i = 0; i < limit; i++) { + final start = baseAddresses[i]; + final end = baseAddresses[i + 1]; + if (start <= address && address < end) { + final fileOffset = address - start + fileOffsets[i]; + return symbolsIndexes[i]?.symbolId(fileOffset); + } + } + return null; + } + + /// Construct [AddressSpaceSymbols] from [Mapping] records loaded from + /// `perf.data`. + static AddressSpaceSymbols fromMappings( + List mappings, ProfileBuilder profileBuilder) { + // Try loading symbols for each mapping and keep those that + // actually have symbols. Sort resulting list by base address. + final mappingsWithSymbols = <(Mapping, SymbolsIndex)>[]; + for (var event in mappings) { + final symbolsIndex = profileBuilder.symbolsIndexFor(event.path); + if (symbolsIndex != null) { + mappingsWithSymbols.add((event, symbolsIndex)); + } + } + mappingsWithSymbols + .sort((a, b) => a.$1.baseAddress.compareTo(b.$1.baseAddress)); + + // Build `AddressSpaceSymbols` from mappings with symbols. + // + // Note: we need to accomodate for a situation when two mappings are + // adjacent. However we assume that number of mappings is rather small + // so we don't optimize this code too much. + final result = <({int baseAddress, SymbolsIndex? index, int fileOffset})>[]; + void addEntry({ + required int baseAddress, + required SymbolsIndex? index, + required int fileOffset, + }) { + if (result.isNotEmpty && result.last.baseAddress == baseAddress) { + // Collapse end of the previous mapping and the start of the new + // mapping. + if (result.last.index != null) { + throw StateError('Unexpected intersection of address ranges'); + } + result.removeLast(); + } + result.add( + (baseAddress: baseAddress, index: index, fileOffset: fileOffset)); + } + + addEntry(baseAddress: 0, index: null, fileOffset: 0); + for (var e in mappingsWithSymbols) { + addEntry( + baseAddress: e.$1.baseAddress, + index: e.$2, + fileOffset: e.$1.offset, + ); + addEntry( + baseAddress: e.$1.baseAddress + e.$1.length, + index: null, + fileOffset: 0, + ); + } + + // Split result into individual components. + return AddressSpaceSymbols._( + Int64List.fromList( + result.map((e) => e.baseAddress).toList(growable: false), + ), + result.map((e) => e.index).toList(growable: false), + Int64List.fromList( + result.map((e) => e.fileOffset).toList(growable: false), + ), + ); + } +} + +/// A trie node representing a callstack frame. +/// +/// To minimize the size of the produced `pprof.profile` we collapse all +/// matching callstacks into a single `pprof.Sample` entry in the profile. +/// This is done by through a simple [trie][1] data structure. +/// +/// Nodes corresponding to callstacks from original profile will have not-null +/// non-zero [totalBytes] associated with them. Path to these nodes should +/// be flushed into [pprof.Profile] as individual [pprof.Sample] entries +/// at the end of conversion. See [flushTo]. +/// +/// [1]: https://en.wikipedia.org/wiki/Trie +final class CallStackTrieNode { + /// [pprof.Profile] location id corresponding to this frame. + final Int64 id; + + /// Total number of bytes allocated by this frame. + int totalBytes = 0; + + /// Callees of this frame. + late final Map children = + {}; + + CallStackTrieNode({required this.id}); + + CallStackTrieNode operator [](Int64 id) => + children[id] ??= CallStackTrieNode(id: id); + + void flushTo(pprof.Profile profile, List path) { + if (totalBytes != 0) { + profile.sample.add( + pprof.Sample(locationId: path.reversed)..value.add(Int64(totalBytes)), + ); + } + for (var child in children.values) { + path.add(child.id); + child.flushTo(profile, path); + path.removeLast(); + } + } +} + +/// Helper for building [pprof.Profile]. +/// +/// It takes care of indexing symbols and managing their ids. +final class ProfileBuilder { + final profile = pprof.Profile(); + + final symbolTable = {}; + final locationIds = {}; + + final callStackTrieRoot = CallStackTrieNode(id: Int64(-1)); + + ProfileBuilder() { + addString(""); + profile.sampleType.add(pprof.ValueType( + type: addString('space'), + unit: addString('bytes'), + )); + } + + Int64 addString(String str) { + var id = symbolTable[str]; + if (id != null) { + return id; + } + id = symbolTable[str] = Int64(symbolTable.length); + profile.stringTable.add(str); + return id; + } + + Int64 addSymbol(String symbol) { + var id = locationIds[symbol]; + if (id != null) { + return id; + } + id = locationIds[symbol] = Int64(locationIds.length + 1); + profile.function.add(pprof.Function_(id: id, name: addString(symbol))); + profile.location + .add(pprof.Location(id: id, line: [pprof.Line(functionId: id)])); + return id; + } + + SymbolsIndex? symbolsIndexFor(String path) { + final symbols = Symbols.load(path); + if (symbols == null) { + return null; + } + return SymbolsIndex(this, symbols); + } + + pprof.Profile finishProfile() { + callStackTrieRoot.flushTo(profile, []); + return profile; + } +} + +/// Helper for converting raw callstack into its symbolized form. +/// +/// We assume that callstack for each new sample usually shares its prefix +/// (e.g. outermost callers, like `main`) with the previously processed +/// sample. This allows us to reuse location ids from the previous sample +/// for the large portion of the stack. +final class SymbolizedCallStackBuilder { + /// Depth of the current stack. + int depth = 0; + + /// Raw addresses for each frame in the caller to callee order. + /// + /// We do not clear this array between samples allowing us to detect + /// situations when we can reuse entries. Only entries `0..depth-1` + /// correspond to the current stack. Other entries originate from + /// previous samples and might be out of sync with the current sample. + /// + /// (`0` is the outermost caller, `1` is its callee, etc). + final pcs = Int64List(200); + + /// Trie nodes corresponding to each frame in the stack. + /// + /// For entries in the `0..depth-2` range `nodes[i]` is parent of + /// `nodes[i+1]` . + /// + final nodes = List.filled(200, null); + + /// Trie node for the last frame (either `nodes[depth-1]` or root trie node + /// if [depth] is `0`). + CallStackTrieNode last; + + /// `true` when the callstack which is currently being built matches + /// the prefix of the previous callstack. + bool prefixMatches = true; + + SymbolizedCallStackBuilder(ProfileBuilder builder) + : last = builder.callStackTrieRoot; + + void add(int pc, AddressSpaceSymbols syms) { + if (pcs[depth] != pc) { + // Mismatch between newly added `pc` and the `pc` we have from the + // previous sample. We need to lookup location id for it. + final id = syms.symbolId(pc); + if (id == null) { + // No symbol - drop the frame. + return; + } + + pcs[depth] = pc; + last = last[id]; + + // We might still hit the same node in the trie. + if (nodes[depth] != last) { + nodes[depth] = last; + // From here onward we can't reuse `nodes` because the path has + // diverged. + prefixMatches = false; + } + } else if (!prefixMatches) { + // Address might match - but the path we got here might be different. This + // means we can reuse `id` from the node, but not the node itself. + final id = nodes[depth]!.id; + nodes[depth] = last = last[id]; + } else { + // This pc *and* the all previous nodes match. We can just reuse + // the node. + last = nodes[depth]!; + } + depth++; + } + + void addTo(ProfileBuilder builder, int allocatedBytes) { + last.totalBytes += allocatedBytes; + } + + void reset(ProfileBuilder builder) { + depth = 0; + last = builder.callStackTrieRoot; + prefixMatches = true; + } +} + +@pragma('vm:never-inline') +pprof.Profile buildProfileFromPerfData(String path) { + final raf = File(path).openSync(); + + final profileBuilder = ProfileBuilder(); + final perfData = PerfData(raf); + + // Check that input file has expected format. + final allAttrs = perfData.readAttrs(); + if (allAttrs.length != 1) { + perfData.reportError( + 'Expected single perf_event_attrs structure, got ${allAttrs.length}'); + } + + final attrs = allAttrs.first; + + if (attrs.type != TypeId.tracepoint) { + perfData.reportError('Expected to find a file with tracepoint events'); + } + + const expectedSampleFormat = SampleFormat.ip | + SampleFormat.tid | + SampleFormat.time | + SampleFormat.callchain | + SampleFormat.cpu | + SampleFormat.period | + SampleFormat.raw; + if (attrs.sampleType != expectedSampleFormat) { + perfData.reportError( + 'Expected to sample format ${SampleFormat.format(expectedSampleFormat)}' + ' got ${SampleFormat.format(attrs.sampleType)}: difference ' + '${SampleFormat.format(attrs.sampleType ^ expectedSampleFormat)}'); + } + + final mappings = []; + perfData.readEvents((type, chunk, pos) { + if (type == EventType.mmap2) { + final event = Struct.create(chunk, pos); + mappings.add(Mapping( + baseAddress: event.addr, + length: event.len, + path: event.filename.toStringFromZeroTerminated(), + offset: event.pgoffs, + )); + } else if (type == EventType.sample && mappings.isNotEmpty) { + // TODO: we miss one sample here. + return false; // Break iteration. + } + return true; + }); + + final syms = AddressSpaceSymbols.fromMappings(mappings, profileBuilder); + final stack = SymbolizedCallStackBuilder(profileBuilder); + perfData.readEvents((type, chunk, pos) { + if (type == EventType.sample) { + final sample = Struct.create(chunk, pos); + final probeData = Struct.create( + chunk, pos + sizeOf() + sample.nr * 8); + + for (var i = sample.nr - 1; i > 1; i--) { + stack.add(sample.ips[i], syms); + } + if (stack.depth > 0) { + // Accumulate [totalBytes] in the last node. + stack.last.totalBytes += probeData.top - probeData.addr - 1; + } + + // Reset the stack for the next sample. + stack.reset(profileBuilder); + } + + return true; + }); + + print('All data loaded - creating profile.'); + return profileBuilder.finishProfile(); +} + +void main(List args) async { + final perfDataPath = args[0]; + print('loading $perfDataPath'); + final profile = buildProfileFromPerfData(perfDataPath); + print('created ${profile.sample.length} samples'); + + print('Serializing proto (pprof.profile)'); + final result = profile.writeToBuffer(); + print('... ${result.length} bytes'); + File('pprof.profile').writeAsBytesSync(result); + print('Done'); +} diff --git a/runtime/tools/profiling/bin/set_uprobe.dart b/runtime/tools/profiling/bin/set_uprobe.dart new file mode 100644 index 00000000000..811767ed22a --- /dev/null +++ b/runtime/tools/profiling/bin/set_uprobe.dart @@ -0,0 +1,135 @@ +// 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. + +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import 'package:profiling/src/elf_utils.dart'; + +// TODO(vegorov): update this to support Android ARM64 both for standalone +// binaries and Flutter applications. Prototype code for that is available +// in https://dart-review.googlesource.com/c/sdk/+/239661. +void main(List args) async { + if (args.length != 3) { + print( + 'Usage: pkg/vm/tool/set_uprobe.dart '); + exit(-1); + } + + final [probeName, symbol, sharedObject] = args; + + final uprobeAddress = + await _computeProbesVirtualAddress(sharedObject, symbol); + final loadingBias = loadingBiasOf(sharedObject); + + final uprobeFileOffset = (uprobeAddress + loadingBias).toRadixString(16); + + final soName = p.basename(sharedObject); + final soPath = p.canonicalize(p.absolute(sharedObject)); + + // TODO(vegorov) ARM64 support + final threadRegister = "r14"; + final resultRegister = "ax"; + + final uprobeFormat = symbol == 'AllocationProbePoint' + ? 'addr=%$resultRegister:s64 top=+${await _getThreadTopOffset()}(%$threadRegister):s64 cid=-1(%$resultRegister):b20@12/32' + : ''; + + final probe = 'p:$probeName $soPath:0x$uprobeFileOffset $uprobeFormat'; + print(probe); + + File('/sys/kernel/tracing/uprobe_events').writeAsStringSync(probe); +} + +Future _computeProbesVirtualAddress( + String sharedObject, String targetSymbol) async { + int offset = 0; + if (targetSymbol == 'AllocationProbePoint') { + offset = await _determineAllocProbeOffset(sharedObject); + } + + final targetRe = RegExp('\\b$targetSymbol\\b'); + final matches = { + for (final (:addr, :name) in textSymbolsOf(sharedObject)) + if (targetRe.hasMatch(name)) name: addr, + }; + + if (matches.isEmpty) { + throw 'Symbol $targetSymbol not found in $sharedObject'; + } + + if (matches.length != 1) { + throw 'Multiple symbols match: ${matches.keys}'; + } + + final entry = matches.entries.single; + print('placing uprobe on ${entry.key} at ' + '0x${entry.value.toRadixString(16)}+$offset'); + return entry.value + offset; +} + +// `AllocationProbePoint` stub should have a probe placed at a place where +// stack frame is properly setup so that unwinding succeeds. The stub itself +// contains a dummy test immediate instruction which encodes the offset at +// which the probe should be placed. +Future _determineAllocProbeOffset(String sharedObject) async { + // Dump SO file to get the address of the interesting symbol. + final disassembly = await _exec('llvm-objdump', [ + '--disassemble-symbols=stub AllocationProbePoint', + '-Mintel', + sharedObject, + ]); + + // We are looking for `test al, imm` or `tst x0, #imm` where `imm` is a + // hexadecimal immediate encoding offset to the probe point within the stub. + final pattern = RegExp( + r'^\s+[a-f0-9]+:(( [a-f0-9]{2})+| [a-f0-9]{8})\s+(test|tst)\s+(al|x0),\s+#?0x(?[0-9a-f]+)\s*$', + multiLine: true); + + final match = pattern.firstMatch(disassembly); + if (match == null) { + print(disassembly); + throw StateError( + 'failed to find test-immediate instruction encoding the probe offset'); + } + + return int.parse(match.namedGroup('offset')!, radix: 16); +} + +Future _getThreadTopOffset() async { + // TODO(vegorov) ARM64 support + final sdkSrc = Platform.script.resolve('../../../..').toFilePath(); + await _exec( + 'ninja', ['-C', 'out/ReleaseX64', '-j1000', '-l64', 'offsets_extractor'], + workingDirectory: sdkSrc); + final offsets = + await _exec(p.join(sdkSrc, 'out/ReleaseX64/offsets_extractor'), []); + final line = offsets + .split('\n') + .firstWhere((line) => line.contains('Thread_top_offset')); + final offset = RegExp(r' = (?0x[a-f\d]+);$') + .firstMatch(line)! + .namedGroup('offset')!; + + return int.parse(offset).toString(); +} + +Future _exec(String executable, List args, + {String? workingDirectory}) async { + final result = + await Process.run(executable, args, workingDirectory: workingDirectory); + if (result.exitCode != 0) { + throw StateError(''' +Failed to run $executable ${args.join(' ')} +stdout: +${result.stdout} + +stderr: + +${result.stderr} +'''); + } + return result.stdout as String; +} diff --git a/runtime/tools/profiling/lib/src/elf_utils.dart b/runtime/tools/profiling/lib/src/elf_utils.dart new file mode 100644 index 00000000000..5ba8573ce0d --- /dev/null +++ b/runtime/tools/profiling/lib/src/elf_utils.dart @@ -0,0 +1,44 @@ +// 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. + +import 'dart:io'; + +/// Compute the difference between virtual address and the file offset of the +/// TEXT section. It can be used to convert virtual addresses into +/// file offsets. +int loadingBiasOf(String path) { + final data = Process.runSync('llvm-readelf', ['-l', path]).stdout.split("\n"); + for (var line in data) { + line = line.trim(); + if (line.startsWith("LOAD") && line.contains("R E")) { + final components = line.split(RegExp(r"\s+")); + final fileOffset = int.parse(components[1]); + final virtAddr = int.parse(components[2]); + return virtAddr - fileOffset; + } + } + throw StateError('Unable to determine loading bias for $path'); +} + +/// Iterate over all symbols in TEXT section of the given binary. +Iterable<({int addr, String name})> textSymbolsOf(String path) { + // Run `nm -C` on a binary to extract demangled (-C) symbols. + final output = Process.runSync('/usr/bin/nm', ['-C', path]); + final result = (output.stdout as String).split('\n'); + if (output.exitCode != 0) throw 'failed to run nm'; + + // Parse `nm` output looking for `t` (TEXT) symbols. Each line + // has the following format: + final lineRe = RegExp(r"^(?[0-9a-f]+)\s+(?\w+)\s+(?.*)$"); + // final symbols = <(int, String)>[]; + return result.map((line) { + final m = lineRe.firstMatch(line); + if (m != null && m.namedGroup('typ') == 't') { + final addr = int.parse(m.namedGroup('addr')!, radix: 16); + final name = m.namedGroup('name')!; + return (addr: addr, name: name); + } + return null; + }).nonNulls; +} diff --git a/runtime/tools/profiling/lib/src/perf/perf_data.dart b/runtime/tools/profiling/lib/src/perf/perf_data.dart new file mode 100644 index 00000000000..c5c0c5c5497 --- /dev/null +++ b/runtime/tools/profiling/lib/src/perf/perf_data.dart @@ -0,0 +1,630 @@ +// 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 library file contains data structures and helper methods for parsing +/// `perf.data` files produced by `perf` tool on Linux. +/// +/// Format of this file is documented in: +/// +/// * https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/Documentation/perf.data-file-format.txt +/// * https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h +/// * https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h +/// +library; + +import 'dart:ffi'; +import 'dart:io'; +import 'dart:math' as math; +import 'dart:typed_data'; + +/// `struct perf_header`: header of the `perf.data` file. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h#L64 +final class Header extends Struct { + @Array(8) + external Array magic; + + @Uint64() + external int size; + + @Uint64() + external int attrSize; + + external FileSection attrs; + external FileSection data; + external FileSection eventTypes; + + @Uint64() + external int flags; + + @Array(3) + external Array flags1; +} + +/// `struct perf_file_section`: section inside `perf.data` file. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h#L59 +final class FileSection extends Struct { + @Uint64() + external int offset; + + @Uint64() + external int size; + + @override + String toString() { + return 'PerfFileSection{offset=$offset,size=$size}'; + } +} + +/// Optional sections inside `perf.data` file. +/// +/// The section is present iff corresponding bit in [PerfHeader.flags] is set. +/// +/// `PerfFileSection` descriptors for present sections will follow in sequence +/// immediately after the data section (i.e. the first `PerfFileSection` will +/// be located at `header.data.offset + header.data.size` offset). +/// +/// See [PerfData.readOptionalSectionHeaders]. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/tools/perf/util/header.h#L15 +enum OptionalSection { + reserved, + tracingData, + buildId, + hostname, + osRelease, + version, + arch, + nrCpus, + cpuDesc, + cpuId, + totalMem, + cmdLine, + eventDesc, + cpuTopology, + numaTopology, + branchStack, + groupDesc, + auxTrace, + stat, + cache, + sampleTime, + sampleTopology, + clockId, + dirFormat, + bpfProgInfo, + bpfBtf, + compressed, + cpuPmuCaps, + clockData, + hybridTopology, + pmuCaps +} + +/// `perf_event_header`: common header of all event entries. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L815 +final class EventHeader extends Struct { + @Uint32() + external int type; + + @Uint16() + external int misc; + + @Uint16() + external int size; + + @override + String toString() => 'PerfEventHeader{type=$type,misc=$misc,size=$size}'; +} + +/// `perf_event_attr`: configuration of the event monitored by `perf`. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L389 +final class EventAttr extends Struct { + /// Major type: hardware/software/tracepoint/etc. + /// + /// See [EventType]. + @Uint32() + external int type; + + @Uint32() + external int size; + + /// Type specific configuration information. + @Uint64() + external int config; + + @Uint64() + external int samplePeriodOrFreq; + + @Uint64() + external int sampleType; + + @Uint64() + external int readFormat; + + /// Various bit fields which we currently don't care about. + /// + /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L414 + @Uint64() + external int flags; + + @Uint32() + external int wakeupEventOrWatermark; + @Uint32() + external int bpType; + + /// Union of `bp_addr`/`kprobe_func`/`uprobe_path`/`config1` + @Uint64() + external int config1; + + /// Union of `bp_len`/`kprobe_addr`/`probe_offset`/`config2` + @Uint64() + external int config2; + + /// One of `enum perf_branch_sample_type` + @Uint64() + external int branchSampleType; + + /// Defines set of user regs to dump on samples. + /// See asm/perf_regs.h for details. + @Uint64() + external int sampleRegsUser; + + /// Defines size of the user stack to dump on samples. + @Uint32() + external int sampleStackUser; + + @Int32() + external int clockid; + + /// Defines set of regs to dump for each sample + /// state captured on: + /// - precise = 0: PMU interrupt + /// - precise > 0: sampled instruction + /// + /// See asm/perf_regs.h for details. + @Uint64() + external int sampleRegsIntr; + + /// Wakeup watermark for AUX area + @Uint32() + external int auxWatermark; + @Uint16() + external int sampleMaxStack; + @Uint16() + external int reserved2; + @Uint32() + external int auxSampleSize; + @Uint32() + external int reserved3; + + /// User provided data if sigtrap=1, passed back to user via + /// siginfo_t::si_perf_data, e.g. to permit user to identify the event. + /// Note, siginfo_t::si_perf_data is long-sized, and sig_data will be + /// truncated accordingly on 32 bit architectures. + @Uint64() + external int sigData; + + /// Extension of config2 + @Uint64() + external int config3; +} + +/// `enum perf_event_type`: type of the recorded event. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L838 +extension type const EventType(int _) implements int { + /// `PERF_RECORD_MMAP` + /// + /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L879 + static const mmap = EventType(1); + + /// `PERF_RECORD_SAMPLE` + /// + /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L947 + static const sample = EventType(9); + + /// `PERF_RECORD_MMAP2` + /// + /// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L1035 + static const mmap2 = EventType(10); +} + +/// `enum perf_type_id` +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L29 +extension type const TypeId(int index) implements int { + static const hardware = TypeId(0); + static const software = TypeId(1); + static const tracepoint = TypeId(2); + static const hwCache = TypeId(3); + static const raw = TypeId(4); + static const breakpoint = TypeId(5); +} + +/// `enum perf_event_sample_format`: additional information recorded for sample. +/// +/// Bits that can be set in [PerfEventAttr.sampleType] to request information +/// in the overflow packets. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L139 +extension type const SampleFormat(int bit) implements int { + /// `PERF_SAMPLE_IP` + static const ip = SampleFormat(1 << 0); + + /// `PERF_SAMPLE_TID` + static const tid = SampleFormat(1 << 1); + + /// `PERF_SAMPLE_TIME` + static const time = SampleFormat(1 << 2); + + /// `PERF_SAMPLE_ADDR` + static const addr = SampleFormat(1 << 3); + + /// `PERF_SAMPLE_READ` + static const read = SampleFormat(1 << 4); + + /// `PERF_SAMPLE_CALLCHAIN` + static const callchain = SampleFormat(1 << 5); + + /// `PERF_SAMPLE_ID` + static const id = SampleFormat(1 << 6); + + /// `PERF_SAMPLE_CPU` + static const cpu = SampleFormat(1 << 7); + + /// `PERF_SAMPLE_PERIOD` + static const period = SampleFormat(1 << 8); + + /// `PERF_SAMPLE_STREAM_ID` + static const streamId = SampleFormat(1 << 9); + + /// `PERF_SAMPLE_RAW` + static const raw = SampleFormat(1 << 10); + + /// `PERF_SAMPLE_BRANCH_STACK` + static const branchStack = SampleFormat(1 << 11); + + /// `PERF_SAMPLE_REGS_USER` + static const regsUser = SampleFormat(1 << 12); + + /// `PERF_SAMPLE_STACK_USER` + static const stackUser = SampleFormat(1 << 13); + + /// `PERF_SAMPLE_WEIGHT` + static const weight = SampleFormat(1 << 14); + + /// `PERF_SAMPLE_DATA_SRC` + static const dataSrc = SampleFormat(1 << 15); + + /// `PERF_SAMPLE_IDENTIFIER` + static const identifier = SampleFormat(1 << 16); + + /// `PERF_SAMPLE_TRANSACTION` + static const transaction = SampleFormat(1 << 17); + + /// `PERF_SAMPLE_REGS_INTR` + static const regsIntr = SampleFormat(1 << 18); + + /// `PERF_SAMPLE_PHYS_ADDR` + static const physAddr = SampleFormat(1 << 19); + + /// `PERF_SAMPLE_AUX` + static const aux = SampleFormat(1 << 20); + + /// `PERF_SAMPLE_CGROUP` + static const cgroup = SampleFormat(1 << 21); + + /// `PERF_SAMPLE_DATA_PAGE_SIZE` + static const dataPageSize = SampleFormat(1 << 22); + + /// `PERF_SAMPLE_CODE_PAGE_SIZE` + static const codePageSize = SampleFormat(1 << 23); + + /// `PERF_SAMPLE_WEIGHT_STRUCT` + static const weightStruct = SampleFormat(1 << 24); + + static const bitNames = { + ip: "ip", + tid: "tid", + time: "time", + addr: "addr", + read: "read", + callchain: "callchain", + id: "id", + cpu: "cpu", + period: "period", + streamId: "streamId", + raw: "raw", + branchStack: "branchStack", + regsUser: "regsUser", + stackUser: "stackUser", + weight: "weight", + dataSrc: "dataSrc", + identifier: "identifier", + transaction: "transaction", + regsIntr: "regsIntr", + physAddr: "physAddr", + aux: "aux", + cgroup: "cgroup", + dataPageSize: "dataPageSize", + codePageSize: "codePageSize", + weightStruct: "weightStruct", + }; + + static String format(int mask) { + return SampleFormat.bitNames.entries + .where((e) => (mask & e.key) != 0) + .map((e) => e.value) + .join('|'); + } +} + +/// `PERF_RECORD_MMAP` +/// +/// The `MMAP` events record the `PROT_EXEC` mappings so that we can +/// correlate userspace `IP`s to code. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L879 +final class MmapEvent extends Struct { + external EventHeader header; + + @Uint32() + external int pid; + + @Uint32() + external int tid; + + @Uint64() + external int addr; + + @Uint64() + external int len; + + @Uint64() + external int pgoffs; + + @Array.variable() + external Array filename; + + @override + String toString() => 'MmapEvent{addr=${addr.formatAsAddress()},' + 'len=$len,pgoffs=$pgoffs,' + 'filename=${filename.toStringFromZeroTerminated()}}'; +} + +final class BuildId extends Struct { + @Uint8() + external int size; + + @Uint8() + external int reserved1; + + @Uint16() + external int reserved2; + + @Array(20) + external Array buildId; +} + +final class Ino extends Struct { + @Uint32() + external int maj; + + @Uint32() + external int min; + + @Uint64() + external int ino; + + @Uint64() + external int inoGeneration; +} + +final class BuildIdOrIno extends Union { + external BuildId buildId; + external Ino ino; +} + +/// `PERF_RECORD_MMAP2` +/// +/// The `MMAP2` records are an augmented version of `MMAP` (see [MapEvent]), +/// they add `maj`, `min`, `ino` numbers to be used to uniquely identify each +/// mapping. +/// +/// https://github.com/torvalds/linux/blob/3e9bff3bbe1355805de919f688bef4baefbfd436/include/uapi/linux/perf_event.h#L1035 +final class Mmap2Event extends Struct { + external EventHeader header; + + @Uint32() + external int pid; + + @Uint32() + external int tid; + + @Uint64() + external int addr; + + @Uint64() + external int len; + + @Uint64() + external int pgoffs; + + external BuildIdOrIno buildIdOrIno; + + @Uint32() + external int prot; + + @Uint32() + external int flags; + + @Array.variable() + external Array filename; + + @override + String toString() => 'Mmap2Event{addr=${addr.formatAsAddress()},' + 'len=$len,pgoffs=$pgoffs,' + 'filename=${filename.toStringFromZeroTerminated()}}'; +} + +extension ArrayToString on Array { + String toStringFromFixedLength(int length) => + String.fromCharCodes([for (var i = 0; i < length; i++) this[i]]); + + String toStringFromZeroTerminated() { + final sb = StringBuffer(); + for (var i = 0; this[i] != 0; i++) { + sb.writeCharCode(this[i]); + } + return sb.toString(); + } +} + +extension FormatAsAddress on int { + String formatAsAddress() => toRadixString(16); +} + +const int kb = 1024; +const int mb = 1024 * kb; + +final class StreamingSectionReader { + final RandomAccessFile f; + final FileSection section; + + final chunk = Uint8List(256 * mb); + + /// Number of bytes available in the chunk. + int chunkBytes = 0; + + /// Offset from the start of the section to the start of the chunk. + int chunkOffset = 0; + + /// Position within the chunk. + int pos = 0; + + StreamingSectionReader(this.f, this.section) { + f.setPositionSync(section.offset); + refill(); + } + + bool ensure(int bytes) { + if (chunkBytes < (pos + bytes)) { + refill(); + } + return chunkBytes >= (pos + bytes); + } + + void refill() { + final int leftOverBytes = chunkBytes - pos; + for (int i = 0; i < leftOverBytes; i++) { + chunk[i] = chunk[i + pos]; + } + chunkOffset += pos; + pos = 0; + + // Are there any more bytes left to read? + if (chunkOffset >= section.size) { + chunkBytes = 0; + return; + } + + print( + "processed $chunkOffset bytes of ${section.size} total (${(chunkOffset / section.size * 100).floor()} %)"); + + final bytesAlreadyRead = chunkOffset + leftOverBytes; + final bytesToRead = + math.min(section.size - bytesAlreadyRead, chunk.length - leftOverBytes); + final bytesRead = + f.readIntoSync(chunk, leftOverBytes, leftOverBytes + bytesToRead); + chunkBytes = bytesRead + leftOverBytes; + } +} + +final class PerfData { + final RandomAccessFile f; + + final Header header; + + PerfData(this.f) + : header = Struct.create
(f.readSync(sizeOf
())) { + final magic = header.magic.toStringFromFixedLength(8); + if (magic != 'PERFILE2') { + reportError('Incorrect magic in ${f.path} - $magic'); + } + } + + List readAttrs() { + f.setPositionSync(header.attrs.offset); + final attrs = f.readSync(header.attrs.size); + + final result = []; + int pos = 0; + while (pos + sizeOf() < attrs.length) { + final attr = Struct.create(attrs, pos); + result.add(attr); + pos += attr.size; + } + return result; + } + + Map readOptionalSectionHeaders() { + f.setPositionSync(header.data.offset + header.data.size); + final optionalHeaders = + f.readSync(sizeOf() * OptionalSection.values.length); + + int headerIndex = 0; + return { + for (final flag in OptionalSection.values) + if (header.flags & (1 << flag.index) != 0) + flag: Struct.create( + optionalHeaders, sizeOf() * headerIndex++), + }; + } + + late final _dataReader = StreamingSectionReader(f, header.data); + + @pragma('vm:prefer-inline') + void readEvents(bool Function(int type, Uint8List chunk, int pos) callback) { + final reader = _dataReader; + + while (true) { + if (!reader.ensure(sizeOf())) { + // No more events. + return; + } + + // Note: `reader.ensure` might refill the chunk and invalidate + // created struct so extract values eagerly. + final EventHeader(:type, :size) = + Struct.create(reader.chunk, reader.pos); + if (!reader.ensure(size)) { + return; + } + + // At this point we are guaranteed to have the whole event in the chunk + // starting at `reader.pos`. + if (!callback(type, reader.chunk, reader.pos)) { + reader.pos += size; + return; + } + reader.pos += size; + } + } + + Never reportError(String message) => throw ParseError(f.path, message); +} + +final class ParseError extends Error { + final String file; + final String message; + + ParseError(this.file, this.message); + + @override + String toString() => 'Failed to parse $file: $message'; +} diff --git a/runtime/tools/profiling/lib/src/pprof/generated/profile.pb.dart b/runtime/tools/profiling/lib/src/pprof/generated/profile.pb.dart new file mode 100644 index 00000000000..71199bfd0a3 --- /dev/null +++ b/runtime/tools/profiling/lib/src/pprof/generated/profile.pb.dart @@ -0,0 +1,1151 @@ +// +// Generated code. Do not modify. +// source: profile.proto +// +// @dart = 2.12 + +// ignore_for_file: annotate_overrides, camel_case_types, comment_references +// ignore_for_file: constant_identifier_names, library_prefixes +// ignore_for_file: non_constant_identifier_names, prefer_final_fields +// ignore_for_file: unnecessary_import, unnecessary_this, unused_import + +import 'dart:core' as $core; + +import 'package:fixnum/fixnum.dart' as $fixnum; +import 'package:protobuf/protobuf.dart' as $pb; + +class Profile extends $pb.GeneratedMessage { + factory Profile({ + $core.Iterable? sampleType, + $core.Iterable? sample, + $core.Iterable? mapping, + $core.Iterable? location, + $core.Iterable? function, + $core.Iterable<$core.String>? stringTable, + $fixnum.Int64? dropFrames, + $fixnum.Int64? keepFrames, + $fixnum.Int64? timeNanos, + $fixnum.Int64? durationNanos, + ValueType? periodType, + $fixnum.Int64? period, + $core.Iterable<$fixnum.Int64>? comment, + $fixnum.Int64? defaultSampleType, + }) { + final $result = create(); + if (sampleType != null) { + $result.sampleType.addAll(sampleType); + } + if (sample != null) { + $result.sample.addAll(sample); + } + if (mapping != null) { + $result.mapping.addAll(mapping); + } + if (location != null) { + $result.location.addAll(location); + } + if (function != null) { + $result.function.addAll(function); + } + if (stringTable != null) { + $result.stringTable.addAll(stringTable); + } + if (dropFrames != null) { + $result.dropFrames = dropFrames; + } + if (keepFrames != null) { + $result.keepFrames = keepFrames; + } + if (timeNanos != null) { + $result.timeNanos = timeNanos; + } + if (durationNanos != null) { + $result.durationNanos = durationNanos; + } + if (periodType != null) { + $result.periodType = periodType; + } + if (period != null) { + $result.period = period; + } + if (comment != null) { + $result.comment.addAll(comment); + } + if (defaultSampleType != null) { + $result.defaultSampleType = defaultSampleType; + } + return $result; + } + Profile._() : super(); + factory Profile.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory Profile.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'Profile', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'perfetto.third_party.perftools.profiles'), + createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'sampleType', $pb.PbFieldType.PM, + subBuilder: ValueType.create) + ..pc(2, _omitFieldNames ? '' : 'sample', $pb.PbFieldType.PM, + subBuilder: Sample.create) + ..pc(3, _omitFieldNames ? '' : 'mapping', $pb.PbFieldType.PM, + subBuilder: Mapping.create) + ..pc(4, _omitFieldNames ? '' : 'location', $pb.PbFieldType.PM, + subBuilder: Location.create) + ..pc(5, _omitFieldNames ? '' : 'function', $pb.PbFieldType.PM, + subBuilder: Function_.create) + ..pPS(6, _omitFieldNames ? '' : 'stringTable') + ..aInt64(7, _omitFieldNames ? '' : 'dropFrames') + ..aInt64(8, _omitFieldNames ? '' : 'keepFrames') + ..aInt64(9, _omitFieldNames ? '' : 'timeNanos') + ..aInt64(10, _omitFieldNames ? '' : 'durationNanos') + ..aOM(11, _omitFieldNames ? '' : 'periodType', + subBuilder: ValueType.create) + ..aInt64(12, _omitFieldNames ? '' : 'period') + ..p<$fixnum.Int64>(13, _omitFieldNames ? '' : 'comment', $pb.PbFieldType.K6) + ..aInt64(14, _omitFieldNames ? '' : 'defaultSampleType') + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Profile clone() => Profile()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Profile copyWith(void Function(Profile) updates) => + super.copyWith((message) => updates(message as Profile)) as Profile; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static Profile create() => Profile._(); + Profile createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Profile getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Profile? _defaultInstance; + + /// 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". + @$pb.TagNumber(1) + $core.List get sampleType => $_getList(0); + + /// The set of samples recorded in this profile. + @$pb.TagNumber(2) + $core.List get sample => $_getList(1); + + /// Mapping from address ranges to the image/binary/library mapped + /// into that address range. mapping[0] will be the main binary. + @$pb.TagNumber(3) + $core.List get mapping => $_getList(2); + + /// Useful program location + @$pb.TagNumber(4) + $core.List get location => $_getList(3); + + /// Functions referenced by locations + @$pb.TagNumber(5) + $core.List get function => $_getList(4); + + /// A common table for strings referenced by various messages. + /// string_table[0] must always be "". + @$pb.TagNumber(6) + $core.List<$core.String> get stringTable => $_getList(5); + + /// frames with Function.function_name fully matching the following + /// regexp will be dropped from the samples, along with their successors. + /// Index into string table. + @$pb.TagNumber(7) + $fixnum.Int64 get dropFrames => $_getI64(6); + @$pb.TagNumber(7) + set dropFrames($fixnum.Int64 v) { + $_setInt64(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasDropFrames() => $_has(6); + @$pb.TagNumber(7) + void clearDropFrames() => clearField(7); + + /// frames with Function.function_name fully matching the following + /// regexp will be kept, even if it matches drop_functions. + /// Index into string table. + @$pb.TagNumber(8) + $fixnum.Int64 get keepFrames => $_getI64(7); + @$pb.TagNumber(8) + set keepFrames($fixnum.Int64 v) { + $_setInt64(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasKeepFrames() => $_has(7); + @$pb.TagNumber(8) + void clearKeepFrames() => clearField(8); + + /// Time of collection (UTC) represented as nanoseconds past the epoch. + @$pb.TagNumber(9) + $fixnum.Int64 get timeNanos => $_getI64(8); + @$pb.TagNumber(9) + set timeNanos($fixnum.Int64 v) { + $_setInt64(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasTimeNanos() => $_has(8); + @$pb.TagNumber(9) + void clearTimeNanos() => clearField(9); + + /// Duration of the profile, if a duration makes sense. + @$pb.TagNumber(10) + $fixnum.Int64 get durationNanos => $_getI64(9); + @$pb.TagNumber(10) + set durationNanos($fixnum.Int64 v) { + $_setInt64(9, v); + } + + @$pb.TagNumber(10) + $core.bool hasDurationNanos() => $_has(9); + @$pb.TagNumber(10) + void clearDurationNanos() => clearField(10); + + /// The kind of events between sampled ocurrences. + /// e.g [ "cpu","cycles" ] or [ "heap","bytes" ] + @$pb.TagNumber(11) + ValueType get periodType => $_getN(10); + @$pb.TagNumber(11) + set periodType(ValueType v) { + setField(11, v); + } + + @$pb.TagNumber(11) + $core.bool hasPeriodType() => $_has(10); + @$pb.TagNumber(11) + void clearPeriodType() => clearField(11); + @$pb.TagNumber(11) + ValueType ensurePeriodType() => $_ensure(10); + + /// The number of events between sampled occurrences. + @$pb.TagNumber(12) + $fixnum.Int64 get period => $_getI64(11); + @$pb.TagNumber(12) + set period($fixnum.Int64 v) { + $_setInt64(11, v); + } + + @$pb.TagNumber(12) + $core.bool hasPeriod() => $_has(11); + @$pb.TagNumber(12) + void clearPeriod() => clearField(12); + + /// Freeform text associated to the profile. + /// Indices into string table. + @$pb.TagNumber(13) + $core.List<$fixnum.Int64> get comment => $_getList(12); + + /// Index into the string table of the type of the preferred sample + /// value. If unset, clients should default to the last sample value. + @$pb.TagNumber(14) + $fixnum.Int64 get defaultSampleType => $_getI64(13); + @$pb.TagNumber(14) + set defaultSampleType($fixnum.Int64 v) { + $_setInt64(13, v); + } + + @$pb.TagNumber(14) + $core.bool hasDefaultSampleType() => $_has(13); + @$pb.TagNumber(14) + void clearDefaultSampleType() => clearField(14); +} + +/// ValueType describes the semantics and measurement units of a value. +class ValueType extends $pb.GeneratedMessage { + factory ValueType({ + $fixnum.Int64? type, + $fixnum.Int64? unit, + }) { + final $result = create(); + if (type != null) { + $result.type = type; + } + if (unit != null) { + $result.unit = unit; + } + return $result; + } + ValueType._() : super(); + factory ValueType.fromBuffer($core.List<$core.int> i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromBuffer(i, r); + factory ValueType.fromJson($core.String i, + [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => + create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo( + _omitMessageNames ? '' : 'ValueType', + package: const $pb.PackageName( + _omitMessageNames ? '' : 'perfetto.third_party.perftools.profiles'), + createEmptyInstance: create) + ..aInt64(1, _omitFieldNames ? '' : 'type') + ..aInt64(2, _omitFieldNames ? '' : 'unit') + ..hasRequiredFields = false; + + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ValueType clone() => ValueType()..mergeFromMessage(this); + @$core.Deprecated('Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ValueType copyWith(void Function(ValueType) updates) => + super.copyWith((message) => updates(message as ValueType)) as ValueType; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ValueType create() => ValueType._(); + ValueType createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ValueType getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ValueType? _defaultInstance; + + /// Index into string table. + @$pb.TagNumber(1) + $fixnum.Int64 get type => $_getI64(0); + @$pb.TagNumber(1) + set type($fixnum.Int64 v) { + $_setInt64(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + /// Index into string table. + @$pb.TagNumber(2) + $fixnum.Int64 get unit => $_getI64(1); + @$pb.TagNumber(2) + set unit($fixnum.Int64 v) { + $_setInt64(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasUnit() => $_has(1); + @$pb.TagNumber(2) + void clearUnit() => clearField(2); +} + +/// 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. +class Sample extends $pb.GeneratedMessage { + factory Sample({ + $core.Iterable<$fixnum.Int64>? locationId, + $core.Iterable<$fixnum.Int64>? value, + $core.Iterable