[vm] Allocation profiling via uprobes
This CL adds basic infrastructure and tooling to perform allocation profiling using uprobes. See runtime/tools/profiling/README.md for more details. TEST=manually tested, requires root access Cq-Include-Trybots: luci.dart.try:vm-aot-mac-release-arm64-try Change-Id: Id68d181740dbf227a12d8cdba84b11a9518e75a7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/382405 Commit-Queue: Slava Egorov <vegorov@google.com> Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
committed by
Commit Queue
parent
e1dc36994a
commit
284e9e91c8
@@ -0,0 +1,3 @@
|
||||
# https://dart.dev/guides/libraries/private-files
|
||||
# Created by `dart pub`
|
||||
.dart_tool/
|
||||
@@ -0,0 +1,3 @@
|
||||
## 0.1.0
|
||||
|
||||
- Initial version.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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<Uint64> 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<Int64?> ids;
|
||||
|
||||
SymbolsIndex(this.profileBuilder, this.symbols)
|
||||
: ids = List<Int64?>.filled(symbols.fileOffsets.length, null);
|
||||
|
||||
static final lineRe =
|
||||
RegExp(r"^(?<addr>[0-9a-f]+)\s+(?<typ>\w+)\s+(?<name>.*)$");
|
||||
|
||||
/// 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<SymbolsIndex?> 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<Mapping> 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<Int64, CallStackTrieNode> children =
|
||||
<Int64, CallStackTrieNode>{};
|
||||
|
||||
CallStackTrieNode({required this.id});
|
||||
|
||||
CallStackTrieNode operator [](Int64 id) =>
|
||||
children[id] ??= CallStackTrieNode(id: id);
|
||||
|
||||
void flushTo(pprof.Profile profile, List<Int64> 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 = <String, Int64>{};
|
||||
final locationIds = <String, Int64>{};
|
||||
|
||||
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<CallStackTrieNode?>.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 = <Mapping>[];
|
||||
perfData.readEvents((type, chunk, pos) {
|
||||
if (type == EventType.mmap2) {
|
||||
final event = Struct.create<Mmap2Event>(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<SampleEvent>(chunk, pos);
|
||||
final probeData = Struct.create<ProbeData>(
|
||||
chunk, pos + sizeOf<SampleEvent>() + 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<String> 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');
|
||||
}
|
||||
@@ -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<String> args) async {
|
||||
if (args.length != 3) {
|
||||
print(
|
||||
'Usage: pkg/vm/tool/set_uprobe.dart <probe-name> <symbol> <AOT snapshot SO file>');
|
||||
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<int> _computeProbesVirtualAddress(
|
||||
String sharedObject, String targetSymbol) async {
|
||||
int offset = 0;
|
||||
if (targetSymbol == 'AllocationProbePoint') {
|
||||
offset = await _determineAllocProbeOffset(sharedObject);
|
||||
}
|
||||
|
||||
final targetRe = RegExp('\\b$targetSymbol\\b');
|
||||
final matches = <String, int>{
|
||||
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<int> _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(?<offset>[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<String> _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' = (?<offset>0x[a-f\d]+);$')
|
||||
.firstMatch(line)!
|
||||
.namedGroup('offset')!;
|
||||
|
||||
return int.parse(offset).toString();
|
||||
}
|
||||
|
||||
Future<String> _exec(String executable, List<String> 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;
|
||||
}
|
||||
@@ -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"^(?<addr>[0-9a-f]+)\s+(?<typ>\w+)\s+(?<name>.*)$");
|
||||
// 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;
|
||||
}
|
||||
@@ -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<Uint8> 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<Uint64> 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<Uint8> 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<Uint8> 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<Uint8> filename;
|
||||
|
||||
@override
|
||||
String toString() => 'Mmap2Event{addr=${addr.formatAsAddress()},'
|
||||
'len=$len,pgoffs=$pgoffs,'
|
||||
'filename=${filename.toStringFromZeroTerminated()}}';
|
||||
}
|
||||
|
||||
extension ArrayToString on Array<Uint8> {
|
||||
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<Header>(f.readSync(sizeOf<Header>())) {
|
||||
final magic = header.magic.toStringFromFixedLength(8);
|
||||
if (magic != 'PERFILE2') {
|
||||
reportError('Incorrect magic in ${f.path} - $magic');
|
||||
}
|
||||
}
|
||||
|
||||
List<EventAttr> readAttrs() {
|
||||
f.setPositionSync(header.attrs.offset);
|
||||
final attrs = f.readSync(header.attrs.size);
|
||||
|
||||
final result = <EventAttr>[];
|
||||
int pos = 0;
|
||||
while (pos + sizeOf<EventAttr>() < attrs.length) {
|
||||
final attr = Struct.create<EventAttr>(attrs, pos);
|
||||
result.add(attr);
|
||||
pos += attr.size;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<OptionalSection, FileSection> readOptionalSectionHeaders() {
|
||||
f.setPositionSync(header.data.offset + header.data.size);
|
||||
final optionalHeaders =
|
||||
f.readSync(sizeOf<FileSection>() * OptionalSection.values.length);
|
||||
|
||||
int headerIndex = 0;
|
||||
return {
|
||||
for (final flag in OptionalSection.values)
|
||||
if (header.flags & (1 << flag.index) != 0)
|
||||
flag: Struct.create<FileSection>(
|
||||
optionalHeaders, sizeOf<FileSection>() * 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<EventHeader>())) {
|
||||
// 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<EventHeader>(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';
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
//
|
||||
// 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
|
||||
@@ -0,0 +1,243 @@
|
||||
//
|
||||
// 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:convert' as $convert;
|
||||
import 'dart:core' as $core;
|
||||
import 'dart:typed_data' as $typed_data;
|
||||
|
||||
@$core.Deprecated('Use profileDescriptor instead')
|
||||
const Profile$json = {
|
||||
'1': 'Profile',
|
||||
'2': [
|
||||
{
|
||||
'1': 'sample_type',
|
||||
'3': 1,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.ValueType',
|
||||
'10': 'sampleType'
|
||||
},
|
||||
{
|
||||
'1': 'sample',
|
||||
'3': 2,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.Sample',
|
||||
'10': 'sample'
|
||||
},
|
||||
{
|
||||
'1': 'mapping',
|
||||
'3': 3,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.Mapping',
|
||||
'10': 'mapping'
|
||||
},
|
||||
{
|
||||
'1': 'location',
|
||||
'3': 4,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.Location',
|
||||
'10': 'location'
|
||||
},
|
||||
{
|
||||
'1': 'function',
|
||||
'3': 5,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.Function',
|
||||
'10': 'function'
|
||||
},
|
||||
{'1': 'string_table', '3': 6, '4': 3, '5': 9, '10': 'stringTable'},
|
||||
{'1': 'drop_frames', '3': 7, '4': 1, '5': 3, '10': 'dropFrames'},
|
||||
{'1': 'keep_frames', '3': 8, '4': 1, '5': 3, '10': 'keepFrames'},
|
||||
{'1': 'time_nanos', '3': 9, '4': 1, '5': 3, '10': 'timeNanos'},
|
||||
{'1': 'duration_nanos', '3': 10, '4': 1, '5': 3, '10': 'durationNanos'},
|
||||
{
|
||||
'1': 'period_type',
|
||||
'3': 11,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.ValueType',
|
||||
'10': 'periodType'
|
||||
},
|
||||
{'1': 'period', '3': 12, '4': 1, '5': 3, '10': 'period'},
|
||||
{'1': 'comment', '3': 13, '4': 3, '5': 3, '10': 'comment'},
|
||||
{
|
||||
'1': 'default_sample_type',
|
||||
'3': 14,
|
||||
'4': 1,
|
||||
'5': 3,
|
||||
'10': 'defaultSampleType'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Profile`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
|
||||
'CgdQcm9maWxlElMKC3NhbXBsZV90eXBlGAEgAygLMjIucGVyZmV0dG8udGhpcmRfcGFydHkucG'
|
||||
'VyZnRvb2xzLnByb2ZpbGVzLlZhbHVlVHlwZVIKc2FtcGxlVHlwZRJHCgZzYW1wbGUYAiADKAsy'
|
||||
'Ly5wZXJmZXR0by50aGlyZF9wYXJ0eS5wZXJmdG9vbHMucHJvZmlsZXMuU2FtcGxlUgZzYW1wbG'
|
||||
'USSgoHbWFwcGluZxgDIAMoCzIwLnBlcmZldHRvLnRoaXJkX3BhcnR5LnBlcmZ0b29scy5wcm9m'
|
||||
'aWxlcy5NYXBwaW5nUgdtYXBwaW5nEk0KCGxvY2F0aW9uGAQgAygLMjEucGVyZmV0dG8udGhpcm'
|
||||
'RfcGFydHkucGVyZnRvb2xzLnByb2ZpbGVzLkxvY2F0aW9uUghsb2NhdGlvbhJNCghmdW5jdGlv'
|
||||
'bhgFIAMoCzIxLnBlcmZldHRvLnRoaXJkX3BhcnR5LnBlcmZ0b29scy5wcm9maWxlcy5GdW5jdG'
|
||||
'lvblIIZnVuY3Rpb24SIQoMc3RyaW5nX3RhYmxlGAYgAygJUgtzdHJpbmdUYWJsZRIfCgtkcm9w'
|
||||
'X2ZyYW1lcxgHIAEoA1IKZHJvcEZyYW1lcxIfCgtrZWVwX2ZyYW1lcxgIIAEoA1IKa2VlcEZyYW'
|
||||
'1lcxIdCgp0aW1lX25hbm9zGAkgASgDUgl0aW1lTmFub3MSJQoOZHVyYXRpb25fbmFub3MYCiAB'
|
||||
'KANSDWR1cmF0aW9uTmFub3MSUwoLcGVyaW9kX3R5cGUYCyABKAsyMi5wZXJmZXR0by50aGlyZF'
|
||||
'9wYXJ0eS5wZXJmdG9vbHMucHJvZmlsZXMuVmFsdWVUeXBlUgpwZXJpb2RUeXBlEhYKBnBlcmlv'
|
||||
'ZBgMIAEoA1IGcGVyaW9kEhgKB2NvbW1lbnQYDSADKANSB2NvbW1lbnQSLgoTZGVmYXVsdF9zYW'
|
||||
'1wbGVfdHlwZRgOIAEoA1IRZGVmYXVsdFNhbXBsZVR5cGU=');
|
||||
|
||||
@$core.Deprecated('Use valueTypeDescriptor instead')
|
||||
const ValueType$json = {
|
||||
'1': 'ValueType',
|
||||
'2': [
|
||||
{'1': 'type', '3': 1, '4': 1, '5': 3, '10': 'type'},
|
||||
{'1': 'unit', '3': 2, '4': 1, '5': 3, '10': 'unit'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `ValueType`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List valueTypeDescriptor = $convert.base64Decode(
|
||||
'CglWYWx1ZVR5cGUSEgoEdHlwZRgBIAEoA1IEdHlwZRISCgR1bml0GAIgASgDUgR1bml0');
|
||||
|
||||
@$core.Deprecated('Use sampleDescriptor instead')
|
||||
const Sample$json = {
|
||||
'1': 'Sample',
|
||||
'2': [
|
||||
{'1': 'location_id', '3': 1, '4': 3, '5': 4, '10': 'locationId'},
|
||||
{'1': 'value', '3': 2, '4': 3, '5': 3, '10': 'value'},
|
||||
{
|
||||
'1': 'label',
|
||||
'3': 3,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.Label',
|
||||
'10': 'label'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Sample`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List sampleDescriptor = $convert.base64Decode(
|
||||
'CgZTYW1wbGUSHwoLbG9jYXRpb25faWQYASADKARSCmxvY2F0aW9uSWQSFAoFdmFsdWUYAiADKA'
|
||||
'NSBXZhbHVlEkQKBWxhYmVsGAMgAygLMi4ucGVyZmV0dG8udGhpcmRfcGFydHkucGVyZnRvb2xz'
|
||||
'LnByb2ZpbGVzLkxhYmVsUgVsYWJlbA==');
|
||||
|
||||
@$core.Deprecated('Use labelDescriptor instead')
|
||||
const Label$json = {
|
||||
'1': 'Label',
|
||||
'2': [
|
||||
{'1': 'key', '3': 1, '4': 1, '5': 3, '10': 'key'},
|
||||
{'1': 'str', '3': 2, '4': 1, '5': 3, '10': 'str'},
|
||||
{'1': 'num', '3': 3, '4': 1, '5': 3, '10': 'num'},
|
||||
{'1': 'num_unit', '3': 4, '4': 1, '5': 3, '10': 'numUnit'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Label`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List labelDescriptor = $convert.base64Decode(
|
||||
'CgVMYWJlbBIQCgNrZXkYASABKANSA2tleRIQCgNzdHIYAiABKANSA3N0chIQCgNudW0YAyABKA'
|
||||
'NSA251bRIZCghudW1fdW5pdBgEIAEoA1IHbnVtVW5pdA==');
|
||||
|
||||
@$core.Deprecated('Use mappingDescriptor instead')
|
||||
const Mapping$json = {
|
||||
'1': 'Mapping',
|
||||
'2': [
|
||||
{'1': 'id', '3': 1, '4': 1, '5': 4, '10': 'id'},
|
||||
{'1': 'memory_start', '3': 2, '4': 1, '5': 4, '10': 'memoryStart'},
|
||||
{'1': 'memory_limit', '3': 3, '4': 1, '5': 4, '10': 'memoryLimit'},
|
||||
{'1': 'file_offset', '3': 4, '4': 1, '5': 4, '10': 'fileOffset'},
|
||||
{'1': 'filename', '3': 5, '4': 1, '5': 3, '10': 'filename'},
|
||||
{'1': 'build_id', '3': 6, '4': 1, '5': 3, '10': 'buildId'},
|
||||
{'1': 'has_functions', '3': 7, '4': 1, '5': 8, '10': 'hasFunctions'},
|
||||
{'1': 'has_filenames', '3': 8, '4': 1, '5': 8, '10': 'hasFilenames'},
|
||||
{'1': 'has_line_numbers', '3': 9, '4': 1, '5': 8, '10': 'hasLineNumbers'},
|
||||
{
|
||||
'1': 'has_inline_frames',
|
||||
'3': 10,
|
||||
'4': 1,
|
||||
'5': 8,
|
||||
'10': 'hasInlineFrames'
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Mapping`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List mappingDescriptor = $convert.base64Decode(
|
||||
'CgdNYXBwaW5nEg4KAmlkGAEgASgEUgJpZBIhCgxtZW1vcnlfc3RhcnQYAiABKARSC21lbW9yeV'
|
||||
'N0YXJ0EiEKDG1lbW9yeV9saW1pdBgDIAEoBFILbWVtb3J5TGltaXQSHwoLZmlsZV9vZmZzZXQY'
|
||||
'BCABKARSCmZpbGVPZmZzZXQSGgoIZmlsZW5hbWUYBSABKANSCGZpbGVuYW1lEhkKCGJ1aWxkX2'
|
||||
'lkGAYgASgDUgdidWlsZElkEiMKDWhhc19mdW5jdGlvbnMYByABKAhSDGhhc0Z1bmN0aW9ucxIj'
|
||||
'Cg1oYXNfZmlsZW5hbWVzGAggASgIUgxoYXNGaWxlbmFtZXMSKAoQaGFzX2xpbmVfbnVtYmVycx'
|
||||
'gJIAEoCFIOaGFzTGluZU51bWJlcnMSKgoRaGFzX2lubGluZV9mcmFtZXMYCiABKAhSD2hhc0lu'
|
||||
'bGluZUZyYW1lcw==');
|
||||
|
||||
@$core.Deprecated('Use locationDescriptor instead')
|
||||
const Location$json = {
|
||||
'1': 'Location',
|
||||
'2': [
|
||||
{'1': 'id', '3': 1, '4': 1, '5': 4, '10': 'id'},
|
||||
{'1': 'mapping_id', '3': 2, '4': 1, '5': 4, '10': 'mappingId'},
|
||||
{'1': 'address', '3': 3, '4': 1, '5': 4, '10': 'address'},
|
||||
{
|
||||
'1': 'line',
|
||||
'3': 4,
|
||||
'4': 3,
|
||||
'5': 11,
|
||||
'6': '.perfetto.third_party.perftools.profiles.Line',
|
||||
'10': 'line'
|
||||
},
|
||||
{'1': 'is_folded', '3': 5, '4': 1, '5': 8, '10': 'isFolded'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Location`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List locationDescriptor = $convert.base64Decode(
|
||||
'CghMb2NhdGlvbhIOCgJpZBgBIAEoBFICaWQSHQoKbWFwcGluZ19pZBgCIAEoBFIJbWFwcGluZ0'
|
||||
'lkEhgKB2FkZHJlc3MYAyABKARSB2FkZHJlc3MSQQoEbGluZRgEIAMoCzItLnBlcmZldHRvLnRo'
|
||||
'aXJkX3BhcnR5LnBlcmZ0b29scy5wcm9maWxlcy5MaW5lUgRsaW5lEhsKCWlzX2ZvbGRlZBgFIA'
|
||||
'EoCFIIaXNGb2xkZWQ=');
|
||||
|
||||
@$core.Deprecated('Use lineDescriptor instead')
|
||||
const Line$json = {
|
||||
'1': 'Line',
|
||||
'2': [
|
||||
{'1': 'function_id', '3': 1, '4': 1, '5': 4, '10': 'functionId'},
|
||||
{'1': 'line', '3': 2, '4': 1, '5': 3, '10': 'line'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Line`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List lineDescriptor = $convert.base64Decode(
|
||||
'CgRMaW5lEh8KC2Z1bmN0aW9uX2lkGAEgASgEUgpmdW5jdGlvbklkEhIKBGxpbmUYAiABKANSBG'
|
||||
'xpbmU=');
|
||||
|
||||
@$core.Deprecated('Use function_Descriptor instead')
|
||||
const Function_$json = {
|
||||
'1': 'Function',
|
||||
'2': [
|
||||
{'1': 'id', '3': 1, '4': 1, '5': 4, '10': 'id'},
|
||||
{'1': 'name', '3': 2, '4': 1, '5': 3, '10': 'name'},
|
||||
{'1': 'system_name', '3': 3, '4': 1, '5': 3, '10': 'systemName'},
|
||||
{'1': 'filename', '3': 4, '4': 1, '5': 3, '10': 'filename'},
|
||||
{'1': 'start_line', '3': 5, '4': 1, '5': 3, '10': 'startLine'},
|
||||
],
|
||||
};
|
||||
|
||||
/// Descriptor for `Function`. Decode as a `google.protobuf.DescriptorProto`.
|
||||
final $typed_data.Uint8List function_Descriptor = $convert.base64Decode(
|
||||
'CghGdW5jdGlvbhIOCgJpZBgBIAEoBFICaWQSEgoEbmFtZRgCIAEoA1IEbmFtZRIfCgtzeXN0ZW'
|
||||
'1fbmFtZRgDIAEoA1IKc3lzdGVtTmFtZRIaCghmaWxlbmFtZRgEIAEoA1IIZmlsZW5hbWUSHQoK'
|
||||
'c3RhcnRfbGluZRgFIAEoA1IJc3RhcnRMaW5l');
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// 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
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, library_prefixes
|
||||
// ignore_for_file: non_constant_identifier_names, prefer_final_fields
|
||||
// ignore_for_file: unnecessary_import, unnecessary_this, unused_import
|
||||
|
||||
export 'profile.pb.dart';
|
||||
@@ -0,0 +1,230 @@
|
||||
// Copyright (C) 2018 The Android Open Source Project
|
||||
//
|
||||
// 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";
|
||||
|
||||
// This is in perfetto.third_party to avoid clashing with potential other
|
||||
// copies of this proto.
|
||||
package perfetto.third_party.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;
|
||||
// Useful program location
|
||||
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.
|
||||
// Index into string table.
|
||||
int64 drop_frames = 7;
|
||||
// frames with Function.function_name fully matching the following
|
||||
// regexp will be kept, even if it matches drop_functions.
|
||||
// Index into string table.
|
||||
int64 keep_frames = 8;
|
||||
|
||||
// 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 ocurrences.
|
||||
// e.g [ "cpu","cycles" ] or [ "heap","bytes" ]
|
||||
ValueType period_type = 11;
|
||||
// The number of events between sampled occurrences.
|
||||
int64 period = 12;
|
||||
// Freeform text associated to the profile.
|
||||
// Indices into string table.
|
||||
repeated int64 comment = 13;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ValueType describes the semantics and measurement units of a value.
|
||||
message ValueType {
|
||||
// Index into string table.
|
||||
int64 type = 1;
|
||||
// Index into string table.
|
||||
int64 unit = 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.
|
||||
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 elemntwise 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
|
||||
repeated Label label = 3;
|
||||
}
|
||||
|
||||
message Label {
|
||||
// Index into string table
|
||||
int64 key = 1;
|
||||
|
||||
// At most one of the following must be present
|
||||
|
||||
// Index into string table
|
||||
int64 str = 2;
|
||||
|
||||
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.
|
||||
|
||||
// Index into string table
|
||||
int64 num_unit = 4;
|
||||
}
|
||||
|
||||
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]".
|
||||
// Index into string table
|
||||
int64 filename = 5;
|
||||
// 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.
|
||||
// Index into string table
|
||||
int64 build_id = 6;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
message Function {
|
||||
// Unique nonzero id for the function.
|
||||
uint64 id = 1;
|
||||
// Name of the function, in human-readable form if available.
|
||||
// Index into string table
|
||||
int64 name = 2;
|
||||
// Name of the function, as identified by the system.
|
||||
// For instance, it can be a C++ mangled name.
|
||||
// Index into string table
|
||||
int64 system_name = 3;
|
||||
// Source file containing the function.
|
||||
// Index into string table
|
||||
int64 filename = 4;
|
||||
// Line number in source file.
|
||||
int64 start_line = 5;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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:typed_data';
|
||||
|
||||
import 'package:profiling/src/elf_utils.dart';
|
||||
|
||||
/// Symbols of a TEXT section of a binary indexed by their file offset.
|
||||
class Symbols {
|
||||
final Uint32List fileOffsets;
|
||||
final List<String> names;
|
||||
|
||||
Symbols._(this.fileOffsets, this.names);
|
||||
|
||||
/// Given the [fileOffset] find a symbol it falls into.
|
||||
///
|
||||
/// We assume that symbol with index `i` starts at `fileOffsets[i]`
|
||||
/// and ends at `fileOffset[i+1]`.
|
||||
int? symbolIndex(int fileOffset) {
|
||||
int lo = 0;
|
||||
int hi = fileOffsets.length - 1;
|
||||
while (lo <= hi) {
|
||||
int mid = ((hi - lo + 1) >> 1) + lo;
|
||||
if (fileOffset < fileOffsets[mid]) {
|
||||
hi = mid - 1;
|
||||
} else if ((mid != hi) && (fileOffset >= fileOffsets[mid + 1])) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
return mid;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? lookupName(int fileOffset) {
|
||||
final index = symbolIndex(fileOffset);
|
||||
return index != null ? names[index] : null;
|
||||
}
|
||||
|
||||
/// Try loading symbols from the binary at [path].
|
||||
static Symbols? load(String path) {
|
||||
try {
|
||||
final loadingBias = loadingBiasOf(path);
|
||||
final symbols = textSymbolsOf(path).toList(growable: false);
|
||||
if (symbols.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure symbols are sorted to be able to use binary search.
|
||||
symbols.sort((a, b) => a.addr.compareTo(b.addr));
|
||||
|
||||
// `nm` prints virtual addresses - convert these to file offsets
|
||||
// using loading bias.
|
||||
final fileOffsets = Uint32List(symbols.length);
|
||||
final names = List.generate(symbols.length, (i) => symbols[i].name);
|
||||
for (var i = 0; i < symbols.length; i++) {
|
||||
if (symbols[i].addr < loadingBias) {
|
||||
throw StateError(
|
||||
'unexpected: virtual address ${symbols[i].addr} of symbol '
|
||||
'${symbols[i].name} is less than loading bias $loadingBias');
|
||||
}
|
||||
fileOffsets[i] = symbols[i].addr - loadingBias;
|
||||
}
|
||||
|
||||
return Symbols._(fileOffsets, names);
|
||||
} catch (_) {
|
||||
print('failed to load symbols from $path');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
name: profiling
|
||||
description: Utilities for low-level profiling of Dart code
|
||||
version: 0.1.0
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=3.6.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
protobuf: ^3.1.0
|
||||
fixnum: ^1.1.0
|
||||
path: ^1.9.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^4.0.0
|
||||
test: ^1.24.0
|
||||
@@ -3314,6 +3314,12 @@ void StubCodeCompiler::GenerateSubtype7TestCacheStub() {
|
||||
GenerateSubtypeNTestCacheStub(assembler, 7);
|
||||
}
|
||||
|
||||
#ifndef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
void StubCodeCompiler::GenerateAllocationProbePointStub() {
|
||||
__ Stop("allocation probes are not supported on this platform");
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace compiler
|
||||
|
||||
} // namespace dart
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
#define __ assembler->
|
||||
|
||||
namespace dart {
|
||||
#ifdef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
DECLARE_FLAG(bool, generate_probe_points);
|
||||
#endif
|
||||
|
||||
namespace compiler {
|
||||
|
||||
// Ensures that [R0] is a new object, if not it will be added to the remembered
|
||||
@@ -1303,6 +1307,46 @@ void StubCodeCompiler::GenerateNoSuchMethodDispatcherStub() {
|
||||
GenerateNoSuchMethodDispatcherBody(assembler);
|
||||
}
|
||||
|
||||
#ifdef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
void StubCodeCompiler::GenerateAllocationProbePointStub() {
|
||||
if (!FLAG_generate_probe_points) {
|
||||
__ Stop("unexpected invocation of an allocation probe");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a frame on the stack so that we could properly unwind.
|
||||
// Our .eh_frame is very simple and specifies
|
||||
// CFA := FP+16; FP := *(FP+0); LR := *(FP+1);
|
||||
// for the whole .text section.
|
||||
__ EnterStubFrame();
|
||||
// Restore native stack pointer (CSP). Dart SP is currently not
|
||||
// the same because we do not follow ABI which requires native SP
|
||||
// to be 16 bytes aligned. Restoring CSP is important for simpleperf
|
||||
// to be able to unwind the stack - as it copies stack range starting
|
||||
// at CSP before unwinding. If CSP is not restored we copy wrong part
|
||||
// of the stack (CSP is bumped almost to the end of the thread stack).
|
||||
__ andi(CSP, SP, Immediate(~15));
|
||||
// Probe will be placed here by `runtime/tools/profiling/bin/set_uprobe.dart`.
|
||||
const intptr_t probe_offset = __ CodeSize();
|
||||
__ SetupCSPFromThread(THR);
|
||||
__ LeaveStubFrame();
|
||||
__ Ret();
|
||||
// This dummy instruction is encoding offset to the probe point. It will be
|
||||
// used by set_uprobe.dart script.
|
||||
__ TestImmediate(R0, probe_offset);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void InvokeAllocationProbePoint(Assembler* assembler) {
|
||||
#ifdef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
if (FLAG_precompiled_mode && FLAG_generate_probe_points) {
|
||||
__ EnterStubFrame();
|
||||
__ Call(StubCode::AllocationProbePoint());
|
||||
__ LeaveStubFrame();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Called for inline allocation of arrays.
|
||||
// Input registers (preserved):
|
||||
// LR: return address.
|
||||
@@ -1442,6 +1486,7 @@ void StubCodeCompiler::GenerateAllocateArrayStub() {
|
||||
// Done allocating and initializing the array.
|
||||
// AllocateArrayABI::kResultReg: new object.
|
||||
// AllocateArrayABI::kLengthReg: array length as Smi (preserved).
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
// Unable to allocate the array using the fast inline code, just call
|
||||
@@ -1478,6 +1523,7 @@ void StubCodeCompiler::GenerateAllocateMintSharedWithFPURegsStub() {
|
||||
Label slow_case;
|
||||
__ TryAllocate(compiler::MintClass(), &slow_case, Assembler::kNearJump,
|
||||
AllocateMintABI::kResultReg, AllocateMintABI::kTempReg);
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ Ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -1496,6 +1542,7 @@ void StubCodeCompiler::GenerateAllocateMintSharedWithoutFPURegsStub() {
|
||||
Label slow_case;
|
||||
__ TryAllocate(compiler::MintClass(), &slow_case, Assembler::kNearJump,
|
||||
AllocateMintABI::kResultReg, AllocateMintABI::kTempReg);
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ Ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -1923,6 +1970,7 @@ void StubCodeCompiler::GenerateAllocateContextStub() {
|
||||
|
||||
// Done allocating and initializing the context.
|
||||
// R0: new object.
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -1999,6 +2047,7 @@ void StubCodeCompiler::GenerateCloneContextStub() {
|
||||
|
||||
// Done allocating and initializing the context.
|
||||
// R0: new object.
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -2345,6 +2394,7 @@ static void GenerateAllocateObjectHelper(Assembler* assembler,
|
||||
__ Bind(¬_parameterized_case);
|
||||
} // kClsIdReg = R4, kTypeOffsetReg = R5
|
||||
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -3940,6 +3990,7 @@ void StubCodeCompiler::GenerateAllocateTypedDataArrayStub(intptr_t cid) {
|
||||
__ b(&loop, UNSIGNED_LESS);
|
||||
__ WriteAllocationCanary(R1); // Fix overshoot.
|
||||
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ Ret();
|
||||
|
||||
__ Bind(&call_runtime);
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
#define __ assembler->
|
||||
|
||||
namespace dart {
|
||||
|
||||
#ifdef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
DECLARE_FLAG(bool, generate_probe_points);
|
||||
#endif
|
||||
|
||||
namespace compiler {
|
||||
|
||||
// Ensures that [RAX] is a new object, if not it will be added to the remembered
|
||||
@@ -1240,6 +1245,34 @@ void StubCodeCompiler::GenerateNoSuchMethodDispatcherStub() {
|
||||
GenerateNoSuchMethodDispatcherBody(assembler, /*receiver_reg=*/RDX);
|
||||
}
|
||||
|
||||
#ifdef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
void StubCodeCompiler::GenerateAllocationProbePointStub() {
|
||||
if (!FLAG_generate_probe_points) {
|
||||
__ Stop("unexpected invocation of an allocation probe");
|
||||
return;
|
||||
}
|
||||
|
||||
__ EnterStubFrame();
|
||||
// Probe will be placed here by `runtime/tools/profiling/bin/set_uprobe.dart`.
|
||||
const intptr_t probe_offset = __ CodeSize();
|
||||
__ LeaveStubFrame();
|
||||
__ Ret();
|
||||
// This dummy instruction is encoding offset to the probe point. It will be
|
||||
// used by set_uprobe.dart script.
|
||||
__ TestImmediate(RAX, Immediate(probe_offset));
|
||||
}
|
||||
#endif
|
||||
|
||||
static void InvokeAllocationProbePoint(Assembler* assembler) {
|
||||
#ifdef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
if (FLAG_precompiled_mode && FLAG_generate_probe_points) {
|
||||
__ EnterStubFrame();
|
||||
__ Call(StubCode::AllocationProbePoint());
|
||||
__ LeaveStubFrame();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Called for inline allocation of arrays.
|
||||
// Input registers (preserved):
|
||||
// AllocateArrayABI::kLengthReg: array length as Smi.
|
||||
@@ -1360,6 +1393,8 @@ void StubCodeCompiler::GenerateAllocateArrayStub() {
|
||||
__ cmpq(RDI, RCX);
|
||||
__ j(UNSIGNED_LESS, &loop);
|
||||
__ WriteAllocationCanary(RCX);
|
||||
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
// Unable to allocate the array using the fast inline code, just call
|
||||
@@ -1393,6 +1428,7 @@ void StubCodeCompiler::GenerateAllocateMintSharedWithFPURegsStub() {
|
||||
Label slow_case;
|
||||
__ TryAllocate(compiler::MintClass(), &slow_case, Assembler::kNearJump,
|
||||
AllocateMintABI::kResultReg, AllocateMintABI::kTempReg);
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ Ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -1411,6 +1447,7 @@ void StubCodeCompiler::GenerateAllocateMintSharedWithoutFPURegsStub() {
|
||||
Label slow_case;
|
||||
__ TryAllocate(compiler::MintClass(), &slow_case, Assembler::kNearJump,
|
||||
AllocateMintABI::kResultReg, AllocateMintABI::kTempReg);
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ Ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -1859,6 +1896,7 @@ void StubCodeCompiler::GenerateAllocateContextStub() {
|
||||
|
||||
// Done allocating and initializing the context.
|
||||
// RAX: new object.
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -1879,7 +1917,6 @@ void StubCodeCompiler::GenerateAllocateContextStub() {
|
||||
// RAX: new object
|
||||
// Restore the frame pointer.
|
||||
__ LeaveStubFrame();
|
||||
|
||||
__ ret();
|
||||
}
|
||||
|
||||
@@ -1930,6 +1967,7 @@ void StubCodeCompiler::GenerateCloneContextStub() {
|
||||
|
||||
// Done allocating and initializing the context.
|
||||
// RAX: new object.
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -1952,7 +1990,6 @@ void StubCodeCompiler::GenerateCloneContextStub() {
|
||||
// RAX: new object
|
||||
// Restore the frame pointer.
|
||||
__ LeaveStubFrame();
|
||||
|
||||
__ ret();
|
||||
}
|
||||
|
||||
@@ -2266,6 +2303,7 @@ static void GenerateAllocateObjectHelper(Assembler* assembler,
|
||||
__ Bind(¬_parameterized_case);
|
||||
} // kTypeOffsetReg = RDI;
|
||||
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
__ Bind(&slow_case);
|
||||
@@ -3869,6 +3907,7 @@ void StubCodeCompiler::GenerateAllocateTypedDataArrayStub(intptr_t cid) {
|
||||
__ j(UNSIGNED_LESS, &loop, Assembler::kNearJump);
|
||||
|
||||
__ WriteAllocationCanary(RCX); // Fix overshoot.
|
||||
InvokeAllocationProbePoint(assembler);
|
||||
__ ret();
|
||||
|
||||
__ Bind(&call_runtime);
|
||||
|
||||
@@ -24,6 +24,13 @@ namespace dart {
|
||||
|
||||
DECLARE_FLAG(bool, precompiled_mode);
|
||||
|
||||
#ifdef DART_TARGET_SUPPORTS_PROBE_POINTS
|
||||
DEFINE_FLAG(bool,
|
||||
generate_probe_points,
|
||||
false,
|
||||
"Generate probe points for installation of user space probes");
|
||||
#endif
|
||||
|
||||
StubCode::StubCodeEntry StubCode::entries_[kNumStubEntries] = {
|
||||
#if defined(DART_PRECOMPILED_RUNTIME)
|
||||
#define STUB_CODE_DECLARE(name) {nullptr, #name},
|
||||
|
||||
@@ -18,6 +18,14 @@ namespace dart {
|
||||
V(LazySpecializeTypeTest) \
|
||||
V(LazySpecializeNullableTypeTest)
|
||||
|
||||
#if (defined(DART_TARGET_OS_LINUX) || defined(DART_TARGET_OS_ANDROID)) && \
|
||||
(defined(TARGET_ARCH_X64) || defined(TARGET_ARCH_ARM64))
|
||||
// Currently we support probe points only Linux and Android (X64 and ARM64).
|
||||
#define DART_TARGET_SUPPORTS_PROBE_POINTS 1
|
||||
#endif
|
||||
|
||||
#define PROBE_POINT_STUBS_LIST(V) V(AllocationProbePoint)
|
||||
|
||||
// List of stubs created in the VM isolate, these stubs are shared by different
|
||||
// isolates running in this dart process.
|
||||
#define VM_STUB_CODE_LIST(V) \
|
||||
@@ -28,6 +36,7 @@ namespace dart {
|
||||
V(WriteBarrier) \
|
||||
V(WriteBarrierWrappers) \
|
||||
V(ArrayWriteBarrier) \
|
||||
PROBE_POINT_STUBS_LIST(V) \
|
||||
V(AllocateArray) \
|
||||
V(AllocateMint) \
|
||||
V(AllocateDouble) \
|
||||
|
||||
Reference in New Issue
Block a user