[vm] Speedup profile data streaming in AOT

Current implementation of profile data streaming inherited its 
approach to symbolization from the implementation of vm-service's
get{,Perfetto}CpuSamples methods. These methods rather expensive
as they rely on CodeLookupTable to symbolize collected samples, and
constructing CodeLookupTable requires bringing all threads to safepoint
and iterating over old-space to collect code objects. This can take
significant amount of time - especially when old-space is large (e.g.
consider 1Gb+ heaps of Dart Analysis Server).

This CL rewrites profile data streaming to use a completely different
approach in AOT mode where Dart frames are not symbolized eagerly 
and instead stored in the timeline in their raw form: a pair of
an isolate group specific Mapping and a PC value relative to the start
of that mapping. At the end of streaming (or when isolate group
exits) an additional ModuleSymbols packet is emitted which provides
symbolization information for all collected frames. ModuleSymbols
mappings can be cheaply constructed from collected PCs using 
ReversePc lookup tables.

TEST=expanded existing tests

Change-Id: I56ef1dd4c9a17fb0d2e9c24e51f2e4656a6a6964
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/482782
Reviewed-by: Ryan Macnak <rmacnak@google.com>
Commit-Queue: Slava Egorov <vegorov@google.com>
This commit is contained in:
Slava Egorov
2026-02-25 04:48:51 -08:00
committed by Commit Queue
parent a2b1bb421a
commit d7fa1ced6a
30 changed files with 2298 additions and 430 deletions
+1
View File
@@ -21,6 +21,7 @@ dependencies:
# best practice for packages is to specify their compatible version ranges.
# See also https://dart.dev/tools/pub/dependencies.
dev_dependencies:
collection: any
lints: any
test: any
vm_service_protos: any
+44 -5
View File
@@ -12,31 +12,59 @@ import 'package:perf_witness/server.dart';
import 'package:perf_witness/src/async_span.dart';
import 'package:perf_witness/src/common.dart';
import 'simple_hot_loop.dart' deferred as simple_hot_loop;
final parser = ArgParser()
..addOption('tag', abbr: 't', help: 'Tag for the process')
..addFlag(
'start-in-background',
help: 'Start PerfWitnessServer server in background',
)
..addFlag('start-isolate', abbr: 'i', help: 'Start test isolate');
..addFlag('start-isolate', abbr: 'i', help: 'Start test isolate')
..addFlag('no-shutdown', help: 'Do not shutdown PerfWitnessServer')
..addOption('spawn-uri', help: 'Spawn another isolate using spawnUri')
..addFlag('use-deferred', help: 'Use deferred library');
bool shouldStop = false;
Future<void> busyLoop({required String name}) async {
Future<int> busyLoop({required String name, bool useDeferred = false}) async {
if (useDeferred) {
await simple_hot_loop.loadLibrary();
}
print('[$name] BUSY LOOP READY');
var sum = 0;
while (!shouldStop) {
await AsyncSpan.run('sleep', () async {
print(
'[$name] AsyncSpan.create is nop: ${identical(Zone.current, Zone.root)}',
);
await Future.delayed(const Duration(milliseconds: 500));
final sw = Stopwatch()..start();
while (sw.elapsedMilliseconds < 250) {
final l = <int>[];
for (var i = 0; i < 10000; i++) {
l.add(i * i);
}
sum += l[50];
}
if (useDeferred) {
simple_hot_loop.hotLoop(duration: Duration(milliseconds: 100));
}
if (sw.elapsedMilliseconds < 500) {
await Future.delayed(
Duration(milliseconds: 500 - sw.elapsedMilliseconds),
);
}
});
}
print('done');
return sum;
}
void main(List<String> args) async {
print('PID: $pid');
final parsedArgs = parser.parse(args);
// On Windows there is no easy way to send Ctrl-C (SIGINT) to the process
// so we use a keypress instead.
@@ -48,7 +76,12 @@ void main(List<String> args) async {
shouldStop = true;
});
final parsedArgs = parser.parse(args);
final spawnUri = parsedArgs['spawn-uri'] as String?;
Isolate? childIsolate;
if (spawnUri != null) {
childIsolate = await Isolate.spawnUri(Uri.parse(spawnUri), [], null);
}
final tag = parsedArgs['tag'] as String?;
await PerfWitnessServer.start(
tag: tag,
@@ -65,7 +98,13 @@ void main(List<String> args) async {
exit(1);
});
}
await busyLoop(name: 'main');
await busyLoop(name: 'main', useDeferred: parsedArgs.flag('use-deferred'));
if (parsedArgs.flag('no-shutdown')) {
throw 'Abrupt exit without shutdown';
}
if (childIsolate != null) {
childIsolate.kill(priority: Isolate.immediate);
}
await PerfWitnessServer.shutdown();
exit(0);
}
@@ -0,0 +1,20 @@
// Copyright (c) 2026, 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.
int hotLoop({Duration duration = const Duration(seconds: 30)}) {
int sum = 0;
final sw = Stopwatch()..start();
while (sw.elapsed < duration) {
final l = <int>[];
for (var i = 0; i < 10000; i++) {
l.add(i * i);
}
sum += l[50];
}
return sum;
}
void main() {
print(hotLoop());
}
@@ -0,0 +1,213 @@
// Copyright (c) 2026, 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:convert';
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:vm_service_protos/vm_service_protos.dart';
class TraceData {
final trace = Trace();
final seenEvents = <String>{};
final seenStacks = <List<Frame>>{};
final seenTracks = <int>{};
final seenTrackDescriptors = <int>{};
TraceData.fromBytes(Uint8List bytes) {
trace.mergeFromBuffer(bytes);
var state = IncrementalState();
for (var packet in trace.packet) {
if ((packet.sequenceFlags &
TracePacket_SequenceFlags.SEQ_INCREMENTAL_STATE_CLEARED.value) !=
0) {
state = IncrementalState();
}
if (packet.hasInternedData()) {
state.update(packet.internedData);
}
if (packet.hasTrackEvent()) {
final trackEvent = packet.trackEvent;
if (trackEvent.type == TrackEvent_Type.TYPE_SLICE_BEGIN ||
trackEvent.type == TrackEvent_Type.TYPE_INSTANT) {
final name = state.eventNames[packet.trackEvent.nameIid.toInt()]!;
seenEvents.add(name);
seenTracks.add(trackEvent.trackUuid.toInt());
}
}
if (packet.hasModuleSymbols()) {
state.addSymbols(packet.moduleSymbols);
}
if (packet.hasTrackDescriptor()) {
final trackDescriptor = packet.trackDescriptor;
seenTrackDescriptors.add(trackDescriptor.uuid.toInt());
}
if (packet.hasPerfSample()) {
seenStacks.add(state.stacks[packet.perfSample.callstackIid.toInt()]!);
}
}
}
Iterable<List<String>> get flattenedSeenStacks {
return seenStacks.map(
(stack) => stack
.expand((f) => f.functionNames ?? const <String>['<?>'])
.toList(),
);
}
bool hasSeenStack(List<String> expectedStack) {
return seenStacks.firstWhereOrNull(
(stack) => stackMatches(stack, expectedStack),
) !=
null;
}
}
bool stackMatches(List<Frame> stack, List<String> expected) {
var i = 0;
var j = 0;
final expandedStack = stack
.expand(
(f) =>
f.functionNames ??
['${f.iid} ${f.mapping?.iid ?? '?'}@${f.relPc ?? '?'}'],
)
.toList();
while (j < expected.length) {
while (i < expandedStack.length && expandedStack[i] != expected[j]) {
i++;
}
if (i == expandedStack.length) {
return false;
}
j++;
}
return true;
}
class Frame {
final int iid;
List<String>? functionNames;
Mapping? mapping;
int? relPc;
Frame(this.iid);
}
class Mapping {
final int iid;
final String? path;
final String? buildId;
final int start;
final int end;
Mapping({
required this.iid,
this.buildId,
this.path,
required this.start,
required this.end,
});
}
class IncrementalState {
final eventNames = <int, String>{};
final functionNames = <int, String>{};
final frames = <int, Frame>{};
final stacks = <int, List<Frame>>{};
final mappingPaths = <int, String>{};
final mappings = <int, Mapping>{};
final buildIds = <int, String>{};
void update(InternedData internedData) {
for (var eventName in internedData.eventNames) {
eventNames[eventName.iid.toInt()] = eventName.name;
}
for (var functionName in internedData.functionNames) {
functionNames[functionName.iid.toInt()] = utf8.decode(functionName.str);
}
for (var buildId in internedData.buildIds) {
buildIds[buildId.iid.toInt()] = utf8.decode(buildId.str);
}
for (var mappingPath in internedData.mappingPaths) {
mappingPaths[mappingPath.iid.toInt()] = utf8.decode(mappingPath.str);
}
for (var mapping in internedData.mappings) {
// This way of formatting paths matches the way Perfetto UI handles it.
var path = mapping.pathStringIds
.map((id) => mappingPaths[id.toInt()]!)
.join('/');
if (!path.startsWith('/')) {
path = '/$path';
}
mappings[mapping.iid.toInt()] = Mapping(
iid: mapping.iid.toInt(),
start: mapping.start.toInt(),
end: mapping.end.toInt(),
buildId: mapping.buildId.toInt() != 0
? buildIds[mapping.buildId.toInt()]!
: null,
path: path,
);
}
for (var frame in internedData.frames) {
final f = frames[frame.iid.toInt()] ??= Frame(frame.iid.toInt());
final functionNameId = frame.functionNameId.toInt();
if (functionNameId != 0) {
f.functionNames = [functionNames[functionNameId]!];
}
final mappingId = frame.mappingId.toInt();
if (mappingId != 0) {
f.mapping = mappings[mappingId]!;
}
f.relPc = frame.relPc.toInt();
}
for (var stack in internedData.callstacks) {
stacks[stack.iid.toInt()] = stack.frameIds
.map((iid) => frames[iid.toInt()]!)
.toList(growable: false);
}
}
void addSymbols(ModuleSymbols moduleSymbols) {
final buildId = moduleSymbols.buildId;
final symbols = {
for (var s in moduleSymbols.addressSymbols)
s.address.toInt(): [
for (var l in s.lines) l.functionName,
].reversed.toList(),
};
// Resymbolize collected frames using newly added symbols.
for (var frame in frames.values) {
if (frame case Frame(
functionNames: null,
:final mapping?,
:final relPc?,
) when mapping.buildId == buildId && mapping.path == moduleSymbols.path) {
final names = symbols[relPc];
if (names != null && names.isNotEmpty) {
frame.functionNames = names;
}
}
}
}
}
+306 -120
View File
@@ -9,7 +9,8 @@ import 'dart:isolate';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'package:vm_service_protos/vm_service_protos.dart';
import 'common/test_utils.dart';
final packageRoot = p.dirname(
p.dirname(
@@ -91,6 +92,9 @@ class BusyLoopProcess {
bool aot = false,
bool startInBackground = false,
bool overrideDartDataHome = true,
bool noShutdown = false,
bool useDeferred = false,
Uri? spawnUri,
Map<String, String>? environment,
}) async {
final busyLoopArgs = [
@@ -98,24 +102,35 @@ class BusyLoopProcess {
tag,
if (startIsolate) '--start-isolate',
if (startInBackground) '--start-in-background',
if (noShutdown) '--no-shutdown',
if (useDeferred) '--use-deferred',
if (spawnUri != null) ...['--spawn-uri', spawnUri.toString()],
];
final stdout = <String>[];
final String executable;
if (aot) {
executable = p.join(tempDir.path, 'busyLoop.exe');
final result = await io.Process.run(io.Platform.executable, [
final snapshot = p.join(tempDir.path, 'busyLoop.aot');
executable = useDeferred
? p.join(p.dirname(io.Platform.resolvedExecutable), 'dartaotruntime')
: p.join(tempDir.path, 'busyLoop.exe');
if (useDeferred) {
busyLoopArgs.insert(0, snapshot);
}
final result = await io.Process.run(io.Platform.resolvedExecutable, [
'compile',
'exe',
if (useDeferred) 'aot-snapshot' else 'exe',
'-o',
executable,
if (useDeferred) snapshot else executable,
if (useDeferred)
'--extra-gen-snapshot-options=--loading-unit-manifest=${p.join(tempDir.path, 'manifest.json')}',
p.join(testsDir, 'common', 'busy_loop.dart'),
]);
if (result.exitCode != 0) {
throw 'Failed to compile busyLoop script to a binary';
}
} else {
executable = io.Platform.executable;
executable = io.Platform.resolvedExecutable;
busyLoopArgs.insertAll(0, [
'run',
p.join(testsDir, 'common', 'busy_loop.dart'),
@@ -177,7 +192,7 @@ class RecorderProcess {
final stdout = <String>[];
return RecorderProcess._(
await runProcess(
io.Platform.executable,
io.Platform.resolvedExecutable,
[
'run',
p.join(binDir, 'recorder.dart'),
@@ -247,53 +262,19 @@ void main() {
reason: 'Expected timeline file to be created',
);
final trace = Trace()
..mergeFromBuffer(timelineFiles.first.readAsBytesSync());
expect(trace.packet, isNotEmpty);
expect(trace.packet.any((p) => p.hasPerfSample()), isTrue);
// Dart track should be enabled by default.
expect(extractSeenEvents(trace), containsAll(['sleep']));
});
test('end-to-end test with recorder script (AOT)', () async {
final outputDir = io.Directory('${tempDir.path}/output')..createSync();
final busyLoopAotProcess = await BusyLoopProcess.start(
'busy-loop-aot',
tempDir,
aot: true,
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
// Run the recorder in a separate process.
final recorder = await RecorderProcess.start(
tempDir,
outputDir,
tag: 'busy-loop-aot',
);
await Future.delayed(const Duration(seconds: 2));
await recorder.stop();
final timelineFiles = outputDir
.listSync()
.whereType<io.File>()
.where((file) => file.path.endsWith('.timeline'))
.toList();
final timelines = timelineFiles.map((e) => p.basename(e.path)).toList();
expect(
timelines,
equals(['${busyLoopAotProcess.pid}.timeline']),
reason: 'Expected timeline file to be created',
traceData.hasSeenStack([
'busyLoop',
'AsyncSpan.run',
'busyLoop.<anonymous closure>',
]),
isTrue,
);
final trace = Trace()
..mergeFromBuffer(timelineFiles.first.readAsBytesSync());
expect(trace.packet, isNotEmpty);
expect(trace.packet.any((p) => p.hasPerfSample()), isTrue);
// Dart track should be enabled by default.
expect(extractSeenEvents(trace), containsAll(['sleep']));
await busyLoopAotProcess.process.askToExit();
expect(traceData.seenEvents, containsAll(['sleep']));
});
test('end-to-end test with recorder script - early exit', () async {
@@ -319,12 +300,19 @@ void main() {
reason: 'Expected timeline file to be created',
);
final trace = Trace()
..mergeFromBuffer(timelineFiles.first.readAsBytesSync());
expect(trace.packet, isNotEmpty);
expect(trace.packet.any((p) => p.hasPerfSample()), isTrue);
// Dart track should be enabled by default.
expect(extractSeenEvents(trace), containsAll(['sleep']));
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isTrue);
expect(
traceData.hasSeenStack([
'_RawReceivePort._handleMessage',
'_Timer._handleMessage',
'_Timer._runTimers',
]),
isTrue,
);
expect(traceData.seenEvents, containsAll(['sleep']));
});
test('profiler can be disabled', () async {
@@ -352,10 +340,11 @@ void main() {
reason: 'Expected timeline file to be created',
);
final trace = Trace()
..mergeFromBuffer(timelineFiles.first.readAsBytesSync());
expect(trace.packet, isNotEmpty);
expect(trace.packet.any((p) => p.hasPerfSample()), isFalse);
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isFalse);
expect(traceData.seenEvents, containsAll(['sleep']));
});
test('streams can be configured', () async {
@@ -384,15 +373,16 @@ void main() {
reason: 'Expected timeline file to be created',
);
final trace = Trace()
..mergeFromBuffer(timelineFiles.first.readAsBytesSync());
expect(trace.packet, isNotEmpty);
expect(trace.packet.any((p) => p.hasPerfSample()), isFalse);
final seenEvents = extractSeenEvents(trace);
expect(seenEvents, containsAll(['HandleMessage', 'CompileFunction']));
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isFalse);
expect(
traceData.seenEvents,
containsAll(['HandleMessage', 'CompileFunction']),
);
// Dart trace is disabled.
expect(seenEvents, isNot(contains('sleep')));
expect(traceData.seenEvents, isNot(contains('sleep')));
});
test('tag filtering positive test', () async {
@@ -740,11 +730,12 @@ void main() {
reason: 'Expected only new process to be recorded',
);
final trace = Trace()
..mergeFromBuffer(timelineFiles.first.readAsBytesSync());
expect(trace.packet, isNotEmpty);
final seenEvents = extractSeenEvents(trace);
expect(seenEvents, containsAll(['ImportantStartupEvent']));
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isTrue);
expect(traceData.hasSeenStack(['main']), isTrue);
expect(traceData.seenEvents, containsAll(['ImportantStartupEvent']));
await newProcess.process.askToExit();
});
@@ -772,51 +763,246 @@ void main() {
expect(await busyLoopProcess.process.exitCode, 0);
});
});
}
class IncrementalState {
final eventNames = <int, String>{};
void update(InternedData internedData) {
for (var eventName in internedData.eventNames) {
eventNames[eventName.iid.toInt()] = eventName.name;
}
}
}
Set<String> extractSeenEvents(Trace trace) {
var state = IncrementalState();
final seenEvents = <String>{};
final seenTracks = <int>{};
final seenTrackDescriptors = <int>{};
for (var packet in trace.packet) {
if ((packet.sequenceFlags &
TracePacket_SequenceFlags.SEQ_INCREMENTAL_STATE_CLEARED.value) !=
0) {
state = IncrementalState();
}
if (packet.hasInternedData()) {
state.update(packet.internedData);
}
if (packet.hasTrackEvent()) {
final trackEvent = packet.trackEvent;
if (trackEvent.type == TrackEvent_Type.TYPE_SLICE_BEGIN ||
trackEvent.type == TrackEvent_Type.TYPE_INSTANT) {
final name = state.eventNames[packet.trackEvent.nameIid.toInt()]!;
seenEvents.add(name);
}
seenTracks.add(trackEvent.trackUuid.toInt());
}
if (packet.hasTrackDescriptor()) {
final trackDescriptor = packet.trackDescriptor;
seenTrackDescriptors.add(trackDescriptor.uuid.toInt());
}
}
expect(seenTrackDescriptors, containsAll(seenTracks));
return seenEvents;
group('AOT specific', () {
late io.Directory tempDir;
setUp(() async {
tempDir = io.Directory.systemTemp.createTempSync();
});
tearDown(() {
tempDir.deleteSync(recursive: true);
});
test('end-to-end test with recorder script', () async {
final outputDir = io.Directory('${tempDir.path}/output')..createSync();
final busyLoopAotProcess = await BusyLoopProcess.start(
'busy-loop-aot',
tempDir,
aot: true,
);
// Run the recorder in a separate process.
final recorder = await RecorderProcess.start(
tempDir,
outputDir,
tag: 'busy-loop-aot',
);
await Future.delayed(const Duration(seconds: 2));
await recorder.stop();
final timelineFiles = outputDir
.listSync()
.whereType<io.File>()
.where((file) => file.path.endsWith('.timeline'))
.toList();
final timelines = timelineFiles.map((e) => p.basename(e.path)).toList();
expect(
timelines,
equals(['${busyLoopAotProcess.pid}.timeline']),
reason: 'Expected timeline file to be created',
);
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isTrue);
expect(
traceData.hasSeenStack([
'busyLoop',
'AsyncSpan.run',
'busyLoop.<anonymous closure>',
]),
isTrue,
);
expect(traceData.seenEvents, containsAll(['sleep']));
await busyLoopAotProcess.process.askToExit();
});
test('multiple isolate groups', () async {
final outputDir = io.Directory('${tempDir.path}/output')..createSync();
final simpleHotLoopSnapshot = p.join(tempDir.path, 'simple_hot_loop.aot');
final result = await io.Process.run(io.Platform.executable, [
'compile',
'aot-snapshot',
'-o',
simpleHotLoopSnapshot,
p.join(testsDir, 'common', 'simple_hot_loop.dart'),
]);
expect(result.exitCode, 0);
final busyLoopAotProcess = await BusyLoopProcess.start(
'busy-loop-aot',
tempDir,
spawnUri: Uri.file(simpleHotLoopSnapshot),
aot: true,
);
// Run the recorder in a separate process.
final recorder = await RecorderProcess.start(
tempDir,
outputDir,
tag: 'busy-loop-aot',
);
await Future.delayed(const Duration(seconds: 2));
await recorder.stop();
final timelineFiles = outputDir
.listSync()
.whereType<io.File>()
.where((file) => file.path.endsWith('.timeline'))
.toList();
final timelines = timelineFiles.map((e) => p.basename(e.path)).toList();
expect(
timelines,
equals(['${busyLoopAotProcess.pid}.timeline']),
reason: 'Expected timeline file to be created',
);
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isTrue);
expect(
traceData.hasSeenStack([
'busyLoop',
'AsyncSpan.run',
'busyLoop.<anonymous closure>',
]),
isTrue,
);
expect(traceData.hasSeenStack(['main', 'hotLoop']), isTrue);
expect(traceData.seenEvents, containsAll(['sleep']));
await busyLoopAotProcess.process.askToExit();
});
test('exiting without stopping recording', () async {
final outputDir = io.Directory('${tempDir.path}/output')..createSync();
final simpleHotLoopSnapshot = p.join(tempDir.path, 'simple_hot_loop.aot');
final result = await io.Process.run(io.Platform.executable, [
'compile',
'aot-snapshot',
'-o',
simpleHotLoopSnapshot,
p.join(testsDir, 'common', 'simple_hot_loop.dart'),
]);
expect(result.exitCode, 0);
final busyLoopAotProcess = await BusyLoopProcess.start(
'busy-loop-aot',
tempDir,
noShutdown: true,
aot: true,
spawnUri: Uri.file(simpleHotLoopSnapshot),
);
// Run the recorder in a separate process.
final recorder = await RecorderProcess.start(
tempDir,
outputDir,
tag: 'busy-loop-aot',
);
await Future.delayed(const Duration(seconds: 2));
await busyLoopAotProcess.process.askToExit();
await busyLoopAotProcess.process.exitCode;
await recorder.stop();
final timelineFiles = outputDir
.listSync()
.whereType<io.File>()
.where((file) => file.path.endsWith('.timeline'))
.toList();
final timelines = timelineFiles.map((e) => p.basename(e.path)).toList();
expect(
timelines,
equals(['${busyLoopAotProcess.pid}.timeline']),
reason: 'Expected timeline file to be created',
);
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isTrue);
expect(
traceData.hasSeenStack([
'busyLoop',
'AsyncSpan.run',
'busyLoop.<anonymous closure>',
]),
isTrue,
);
expect(traceData.hasSeenStack(['main', 'hotLoop']), isTrue);
expect(traceData.seenEvents, containsAll(['sleep']));
});
test('with deferred units', () async {
final outputDir = io.Directory('${tempDir.path}/output')..createSync();
final busyLoopAotProcess = await BusyLoopProcess.start(
'busy-loop-aot',
tempDir,
aot: true,
useDeferred: true,
);
// Run the recorder in a separate process.
final recorder = await RecorderProcess.start(
tempDir,
outputDir,
tag: 'busy-loop-aot',
);
await Future.delayed(const Duration(seconds: 2));
await recorder.stop();
final timelineFiles = outputDir
.listSync()
.whereType<io.File>()
.where((file) => file.path.endsWith('.timeline'))
.toList();
final timelines = timelineFiles.map((e) => p.basename(e.path)).toList();
expect(
timelines,
equals(['${busyLoopAotProcess.pid}.timeline']),
reason: 'Expected timeline file to be created',
);
final traceData = TraceData.fromBytes(
timelineFiles.first.readAsBytesSync(),
);
expect(traceData.trace.packet.any((p) => p.hasPerfSample()), isTrue);
expect(
traceData.hasSeenStack([
'busyLoop',
'AsyncSpan.run',
'busyLoop.<anonymous closure>',
// Must be able to symbolize a function from a deferred unit.
'hotLoop',
]),
isTrue,
);
expect(traceData.seenEvents, containsAll(['sleep']));
await busyLoopAotProcess.process.askToExit();
});
test('AOT compiled busy loop is recorded', () async {
final busyLoopProcess = await BusyLoopProcess.start(
'busy-loop-tag',
tempDir,
aot: true,
);
await busyLoopProcess.process.askToExit();
expect(await busyLoopProcess.process.exitCode, 0);
});
});
}
@@ -45,6 +45,7 @@ class InternedData extends $pb.GeneratedMessage {
$core.Iterable<$2.InternedString>? functionNames,
$core.Iterable<$2.Frame>? frames,
$core.Iterable<$2.Callstack>? callstacks,
$core.Iterable<$2.InternedString>? buildIds,
$core.Iterable<$2.InternedString>? mappingPaths,
$core.Iterable<$2.Mapping>? mappings,
$core.Iterable<$2.InternedString>? debugAnnotationStringValues,
@@ -57,6 +58,7 @@ class InternedData extends $pb.GeneratedMessage {
if (functionNames != null) result.functionNames.addAll(functionNames);
if (frames != null) result.frames.addAll(frames);
if (callstacks != null) result.callstacks.addAll(callstacks);
if (buildIds != null) result.buildIds.addAll(buildIds);
if (mappingPaths != null) result.mappingPaths.addAll(mappingPaths);
if (mappings != null) result.mappings.addAll(mappings);
if (debugAnnotationStringValues != null)
@@ -91,6 +93,8 @@ class InternedData extends $pb.GeneratedMessage {
subBuilder: $2.Frame.create)
..pPM<$2.Callstack>(7, _omitFieldNames ? '' : 'callstacks',
subBuilder: $2.Callstack.create)
..pPM<$2.InternedString>(16, _omitFieldNames ? '' : 'buildIds',
subBuilder: $2.InternedString.create)
..pPM<$2.InternedString>(17, _omitFieldNames ? '' : 'mappingPaths',
subBuilder: $2.InternedString.create)
..pPM<$2.Mapping>(19, _omitFieldNames ? '' : 'mappings',
@@ -140,17 +144,21 @@ class InternedData extends $pb.GeneratedMessage {
@$pb.TagNumber(7)
$pb.PbList<$2.Callstack> get callstacks => $_getList(5);
/// Build IDs of exectuable files.
@$pb.TagNumber(16)
$pb.PbList<$2.InternedString> get buildIds => $_getList(6);
/// Paths to executable files.
@$pb.TagNumber(17)
$pb.PbList<$2.InternedString> get mappingPaths => $_getList(6);
$pb.PbList<$2.InternedString> get mappingPaths => $_getList(7);
/// Executable files mapped into processes.
@$pb.TagNumber(19)
$pb.PbList<$2.Mapping> get mappings => $_getList(7);
$pb.PbList<$2.Mapping> get mappings => $_getList(8);
/// Interned string values in the DebugAnnotation proto.
@$pb.TagNumber(29)
$pb.PbList<$2.InternedString> get debugAnnotationStringValues => $_getList(8);
$pb.PbList<$2.InternedString> get debugAnnotationStringValues => $_getList(9);
}
const $core.bool _omitFieldNames =
@@ -51,6 +51,14 @@ const InternedData$json = {
'6': '.perfetto.protos.DebugAnnotationName',
'10': 'debugAnnotationNames'
},
{
'1': 'build_ids',
'3': 16,
'4': 3,
'5': 11,
'6': '.perfetto.protos.InternedString',
'10': 'buildIds'
},
{
'1': 'mapping_paths',
'3': 17,
@@ -108,11 +116,12 @@ final $typed_data.Uint8List internedDataDescriptor = $convert.base64Decode(
'Rvcy5FdmVudENhdGVnb3J5Ug9ldmVudENhdGVnb3JpZXMSOwoLZXZlbnRfbmFtZXMYAiADKAsy'
'Gi5wZXJmZXR0by5wcm90b3MuRXZlbnROYW1lUgpldmVudE5hbWVzEloKFmRlYnVnX2Fubm90YX'
'Rpb25fbmFtZXMYAyADKAsyJC5wZXJmZXR0by5wcm90b3MuRGVidWdBbm5vdGF0aW9uTmFtZVIU'
'ZGVidWdBbm5vdGF0aW9uTmFtZXMSRAoNbWFwcGluZ19wYXRocxgRIAMoCzIfLnBlcmZldHRvLn'
'Byb3Rvcy5JbnRlcm5lZFN0cmluZ1IMbWFwcGluZ1BhdGhzEkYKDmZ1bmN0aW9uX25hbWVzGAUg'
'AygLMh8ucGVyZmV0dG8ucHJvdG9zLkludGVybmVkU3RyaW5nUg1mdW5jdGlvbk5hbWVzEjQKCG'
'1hcHBpbmdzGBMgAygLMhgucGVyZmV0dG8ucHJvdG9zLk1hcHBpbmdSCG1hcHBpbmdzEi4KBmZy'
'YW1lcxgGIAMoCzIWLnBlcmZldHRvLnByb3Rvcy5GcmFtZVIGZnJhbWVzEjoKCmNhbGxzdGFja3'
'MYByADKAsyGi5wZXJmZXR0by5wcm90b3MuQ2FsbHN0YWNrUgpjYWxsc3RhY2tzEmQKHmRlYnVn'
'X2Fubm90YXRpb25fc3RyaW5nX3ZhbHVlcxgdIAMoCzIfLnBlcmZldHRvLnByb3Rvcy5JbnRlcm'
'5lZFN0cmluZ1IbZGVidWdBbm5vdGF0aW9uU3RyaW5nVmFsdWVz');
'ZGVidWdBbm5vdGF0aW9uTmFtZXMSPAoJYnVpbGRfaWRzGBAgAygLMh8ucGVyZmV0dG8ucHJvdG'
'9zLkludGVybmVkU3RyaW5nUghidWlsZElkcxJECg1tYXBwaW5nX3BhdGhzGBEgAygLMh8ucGVy'
'ZmV0dG8ucHJvdG9zLkludGVybmVkU3RyaW5nUgxtYXBwaW5nUGF0aHMSRgoOZnVuY3Rpb25fbm'
'FtZXMYBSADKAsyHy5wZXJmZXR0by5wcm90b3MuSW50ZXJuZWRTdHJpbmdSDWZ1bmN0aW9uTmFt'
'ZXMSNAoIbWFwcGluZ3MYEyADKAsyGC5wZXJmZXR0by5wcm90b3MuTWFwcGluZ1IIbWFwcGluZ3'
'MSLgoGZnJhbWVzGAYgAygLMhYucGVyZmV0dG8ucHJvdG9zLkZyYW1lUgZmcmFtZXMSOgoKY2Fs'
'bHN0YWNrcxgHIAMoCzIaLnBlcmZldHRvLnByb3Rvcy5DYWxsc3RhY2tSCmNhbGxzdGFja3MSZA'
'oeZGVidWdfYW5ub3RhdGlvbl9zdHJpbmdfdmFsdWVzGB0gAygLMh8ucGVyZmV0dG8ucHJvdG9z'
'LkludGVybmVkU3RyaW5nUhtkZWJ1Z0Fubm90YXRpb25TdHJpbmdWYWx1ZXM=');
@@ -100,9 +100,240 @@ class InternedString extends $pb.GeneratedMessage {
void clearStr() => $_clearField(2);
}
/// Source line info.
class Line extends $pb.GeneratedMessage {
factory Line({
$core.String? functionName,
$core.String? sourceFileName,
$core.int? lineNumber,
}) {
final result = create();
if (functionName != null) result.functionName = functionName;
if (sourceFileName != null) result.sourceFileName = sourceFileName;
if (lineNumber != null) result.lineNumber = lineNumber;
return result;
}
Line._();
factory Line.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Line.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Line',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'perfetto.protos'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'functionName')
..aOS(2, _omitFieldNames ? '' : 'sourceFileName')
..aI(3, _omitFieldNames ? '' : 'lineNumber', fieldType: $pb.PbFieldType.OU3)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Line clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Line copyWith(void Function(Line) updates) =>
super.copyWith((message) => updates(message as Line)) as Line;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Line create() => Line._();
@$core.override
Line createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Line getDefault() =>
_defaultInstance ??= $pb.GeneratedMessage.$_defaultFor<Line>(create);
static Line? _defaultInstance;
@$pb.TagNumber(1)
$core.String get functionName => $_getSZ(0);
@$pb.TagNumber(1)
set functionName($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasFunctionName() => $_has(0);
@$pb.TagNumber(1)
void clearFunctionName() => $_clearField(1);
@$pb.TagNumber(2)
$core.String get sourceFileName => $_getSZ(1);
@$pb.TagNumber(2)
set sourceFileName($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasSourceFileName() => $_has(1);
@$pb.TagNumber(2)
void clearSourceFileName() => $_clearField(2);
@$pb.TagNumber(3)
$core.int get lineNumber => $_getIZ(2);
@$pb.TagNumber(3)
set lineNumber($core.int value) => $_setUnsignedInt32(2, value);
@$pb.TagNumber(3)
$core.bool hasLineNumber() => $_has(2);
@$pb.TagNumber(3)
void clearLineNumber() => $_clearField(3);
}
/// Symbols for a given address in a module.
class AddressSymbols extends $pb.GeneratedMessage {
factory AddressSymbols({
$fixnum.Int64? address,
$core.Iterable<Line>? lines,
}) {
final result = create();
if (address != null) result.address = address;
if (lines != null) result.lines.addAll(lines);
return result;
}
AddressSymbols._();
factory AddressSymbols.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory AddressSymbols.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'AddressSymbols',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'perfetto.protos'),
createEmptyInstance: create)
..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'address', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..pPM<Line>(2, _omitFieldNames ? '' : 'lines', subBuilder: Line.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AddressSymbols clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
AddressSymbols copyWith(void Function(AddressSymbols) updates) =>
super.copyWith((message) => updates(message as AddressSymbols))
as AddressSymbols;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static AddressSymbols create() => AddressSymbols._();
@$core.override
AddressSymbols createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static AddressSymbols getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<AddressSymbols>(create);
static AddressSymbols? _defaultInstance;
@$pb.TagNumber(1)
$fixnum.Int64 get address => $_getI64(0);
@$pb.TagNumber(1)
set address($fixnum.Int64 value) => $_setInt64(0, value);
@$pb.TagNumber(1)
$core.bool hasAddress() => $_has(0);
@$pb.TagNumber(1)
void clearAddress() => $_clearField(1);
/// Source lines that correspond to this address.
///
/// These are repeated because when inlining happens, multiple functions'
/// frames can be at a single address. Imagine function Foo calling the
/// `std::vector<int>` constructor, which gets inlined at 0xf00. We then get
/// both Foo and the `std::vector<int>` constructor when we symbolize the
/// address.
@$pb.TagNumber(2)
$pb.PbList<Line> get lines => $_getList(1);
}
/// Symbols for addresses seen in a module.
/// Used in re-symbolisation of complete traces.
class ModuleSymbols extends $pb.GeneratedMessage {
factory ModuleSymbols({
$core.String? path,
$core.String? buildId,
$core.Iterable<AddressSymbols>? addressSymbols,
}) {
final result = create();
if (path != null) result.path = path;
if (buildId != null) result.buildId = buildId;
if (addressSymbols != null) result.addressSymbols.addAll(addressSymbols);
return result;
}
ModuleSymbols._();
factory ModuleSymbols.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ModuleSymbols.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ModuleSymbols',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'perfetto.protos'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'path')
..aOS(2, _omitFieldNames ? '' : 'buildId')
..pPM<AddressSymbols>(3, _omitFieldNames ? '' : 'addressSymbols',
subBuilder: AddressSymbols.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ModuleSymbols clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ModuleSymbols copyWith(void Function(ModuleSymbols) updates) =>
super.copyWith((message) => updates(message as ModuleSymbols))
as ModuleSymbols;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ModuleSymbols create() => ModuleSymbols._();
@$core.override
ModuleSymbols createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ModuleSymbols getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ModuleSymbols>(create);
static ModuleSymbols? _defaultInstance;
/// Fully qualified path to the mapping.
/// E.g. /system/lib64/libc.so.
@$pb.TagNumber(1)
$core.String get path => $_getSZ(0);
@$pb.TagNumber(1)
set path($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasPath() => $_has(0);
@$pb.TagNumber(1)
void clearPath() => $_clearField(1);
/// .note.gnu.build-id on Linux (not hex encoded).
/// uuid on MacOS.
/// Module GUID on Windows.
@$pb.TagNumber(2)
$core.String get buildId => $_getSZ(1);
@$pb.TagNumber(2)
set buildId($core.String value) => $_setString(1, value);
@$pb.TagNumber(2)
$core.bool hasBuildId() => $_has(1);
@$pb.TagNumber(2)
void clearBuildId() => $_clearField(2);
@$pb.TagNumber(3)
$pb.PbList<AddressSymbols> get addressSymbols => $_getList(2);
}
class Mapping extends $pb.GeneratedMessage {
factory Mapping({
$fixnum.Int64? iid,
$fixnum.Int64? buildId,
$fixnum.Int64? startOffset,
$fixnum.Int64? start,
$fixnum.Int64? end,
@@ -110,6 +341,7 @@ class Mapping extends $pb.GeneratedMessage {
}) {
final result = create();
if (iid != null) result.iid = iid;
if (buildId != null) result.buildId = buildId;
if (startOffset != null) result.startOffset = startOffset;
if (start != null) result.start = start;
if (end != null) result.end = end;
@@ -133,6 +365,8 @@ class Mapping extends $pb.GeneratedMessage {
createEmptyInstance: create)
..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'iid', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$fixnum.Int64>(2, _omitFieldNames ? '' : 'buildId', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
..a<$fixnum.Int64>(
3, _omitFieldNames ? '' : 'startOffset', $pb.PbFieldType.OU6,
defaultOrMaker: $fixnum.Int64.ZERO)
@@ -172,37 +406,48 @@ class Mapping extends $pb.GeneratedMessage {
@$pb.TagNumber(1)
void clearIid() => $_clearField(1);
/// Interning key.
/// Starts from 1, 0 is the same as "not set".
@$pb.TagNumber(2)
$fixnum.Int64 get buildId => $_getI64(1);
@$pb.TagNumber(2)
set buildId($fixnum.Int64 value) => $_setInt64(1, value);
@$pb.TagNumber(2)
$core.bool hasBuildId() => $_has(1);
@$pb.TagNumber(2)
void clearBuildId() => $_clearField(2);
@$pb.TagNumber(3)
$fixnum.Int64 get startOffset => $_getI64(1);
$fixnum.Int64 get startOffset => $_getI64(2);
@$pb.TagNumber(3)
set startOffset($fixnum.Int64 value) => $_setInt64(1, value);
set startOffset($fixnum.Int64 value) => $_setInt64(2, value);
@$pb.TagNumber(3)
$core.bool hasStartOffset() => $_has(1);
$core.bool hasStartOffset() => $_has(2);
@$pb.TagNumber(3)
void clearStartOffset() => $_clearField(3);
@$pb.TagNumber(4)
$fixnum.Int64 get start => $_getI64(2);
$fixnum.Int64 get start => $_getI64(3);
@$pb.TagNumber(4)
set start($fixnum.Int64 value) => $_setInt64(2, value);
set start($fixnum.Int64 value) => $_setInt64(3, value);
@$pb.TagNumber(4)
$core.bool hasStart() => $_has(2);
$core.bool hasStart() => $_has(3);
@$pb.TagNumber(4)
void clearStart() => $_clearField(4);
@$pb.TagNumber(5)
$fixnum.Int64 get end => $_getI64(3);
$fixnum.Int64 get end => $_getI64(4);
@$pb.TagNumber(5)
set end($fixnum.Int64 value) => $_setInt64(3, value);
set end($fixnum.Int64 value) => $_setInt64(4, value);
@$pb.TagNumber(5)
$core.bool hasEnd() => $_has(3);
$core.bool hasEnd() => $_has(4);
@$pb.TagNumber(5)
void clearEnd() => $_clearField(5);
/// E.g. ["system", "lib64", "libc.so"]
/// id of string.
@$pb.TagNumber(7)
$pb.PbList<$fixnum.Int64> get pathStringIds => $_getList(4);
$pb.PbList<$fixnum.Int64> get pathStringIds => $_getList(5);
}
class Frame extends $pb.GeneratedMessage {
@@ -36,11 +36,72 @@ const InternedString$json = {
final $typed_data.Uint8List internedStringDescriptor = $convert.base64Decode(
'Cg5JbnRlcm5lZFN0cmluZxIQCgNpaWQYASABKARSA2lpZBIQCgNzdHIYAiABKAxSA3N0cg==');
@$core.Deprecated('Use lineDescriptor instead')
const Line$json = {
'1': 'Line',
'2': [
{'1': 'function_name', '3': 1, '4': 1, '5': 9, '10': 'functionName'},
{'1': 'source_file_name', '3': 2, '4': 1, '5': 9, '10': 'sourceFileName'},
{'1': 'line_number', '3': 3, '4': 1, '5': 13, '10': 'lineNumber'},
],
};
/// Descriptor for `Line`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List lineDescriptor = $convert.base64Decode(
'CgRMaW5lEiMKDWZ1bmN0aW9uX25hbWUYASABKAlSDGZ1bmN0aW9uTmFtZRIoChBzb3VyY2VfZm'
'lsZV9uYW1lGAIgASgJUg5zb3VyY2VGaWxlTmFtZRIfCgtsaW5lX251bWJlchgDIAEoDVIKbGlu'
'ZU51bWJlcg==');
@$core.Deprecated('Use addressSymbolsDescriptor instead')
const AddressSymbols$json = {
'1': 'AddressSymbols',
'2': [
{'1': 'address', '3': 1, '4': 1, '5': 4, '10': 'address'},
{
'1': 'lines',
'3': 2,
'4': 3,
'5': 11,
'6': '.perfetto.protos.Line',
'10': 'lines'
},
],
};
/// Descriptor for `AddressSymbols`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List addressSymbolsDescriptor = $convert.base64Decode(
'Cg5BZGRyZXNzU3ltYm9scxIYCgdhZGRyZXNzGAEgASgEUgdhZGRyZXNzEisKBWxpbmVzGAIgAy'
'gLMhUucGVyZmV0dG8ucHJvdG9zLkxpbmVSBWxpbmVz');
@$core.Deprecated('Use moduleSymbolsDescriptor instead')
const ModuleSymbols$json = {
'1': 'ModuleSymbols',
'2': [
{'1': 'path', '3': 1, '4': 1, '5': 9, '10': 'path'},
{'1': 'build_id', '3': 2, '4': 1, '5': 9, '10': 'buildId'},
{
'1': 'address_symbols',
'3': 3,
'4': 3,
'5': 11,
'6': '.perfetto.protos.AddressSymbols',
'10': 'addressSymbols'
},
],
};
/// Descriptor for `ModuleSymbols`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List moduleSymbolsDescriptor = $convert.base64Decode(
'Cg1Nb2R1bGVTeW1ib2xzEhIKBHBhdGgYASABKAlSBHBhdGgSGQoIYnVpbGRfaWQYAiABKAlSB2'
'J1aWxkSWQSSAoPYWRkcmVzc19zeW1ib2xzGAMgAygLMh8ucGVyZmV0dG8ucHJvdG9zLkFkZHJl'
'c3NTeW1ib2xzUg5hZGRyZXNzU3ltYm9scw==');
@$core.Deprecated('Use mappingDescriptor instead')
const Mapping$json = {
'1': 'Mapping',
'2': [
{'1': 'iid', '3': 1, '4': 1, '5': 4, '10': 'iid'},
{'1': 'build_id', '3': 2, '4': 1, '5': 4, '10': 'buildId'},
{'1': 'start_offset', '3': 3, '4': 1, '5': 4, '10': 'startOffset'},
{'1': 'start', '3': 4, '4': 1, '5': 4, '10': 'start'},
{'1': 'end', '3': 5, '4': 1, '5': 4, '10': 'end'},
@@ -50,9 +111,10 @@ const Mapping$json = {
/// Descriptor for `Mapping`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List mappingDescriptor = $convert.base64Decode(
'CgdNYXBwaW5nEhAKA2lpZBgBIAEoBFIDaWlkEiEKDHN0YXJ0X29mZnNldBgDIAEoBFILc3Rhcn'
'RPZmZzZXQSFAoFc3RhcnQYBCABKARSBXN0YXJ0EhAKA2VuZBgFIAEoBFIDZW5kEiYKD3BhdGhf'
'c3RyaW5nX2lkcxgHIAMoBFINcGF0aFN0cmluZ0lkcw==');
'CgdNYXBwaW5nEhAKA2lpZBgBIAEoBFIDaWlkEhkKCGJ1aWxkX2lkGAIgASgEUgdidWlsZElkEi'
'EKDHN0YXJ0X29mZnNldBgDIAEoBFILc3RhcnRPZmZzZXQSFAoFc3RhcnQYBCABKARSBXN0YXJ0'
'EhAKA2VuZBgFIAEoBFIDZW5kEiYKD3BhdGhfc3RyaW5nX2lkcxgHIAMoBFINcGF0aFN0cmluZ0'
'lkcw==');
@$core.Deprecated('Use frameDescriptor instead')
const Frame$json = {
@@ -25,7 +25,8 @@ import 'package:protobuf/protobuf.dart' as $pb;
import 'clock_snapshot.pb.dart' as $0;
import 'interned_data/interned_data.pb.dart' as $2;
import 'profiling/profile_packet.pb.dart' as $4;
import 'profiling/profile_common.pb.dart' as $4;
import 'profiling/profile_packet.pb.dart' as $5;
import 'track_event/track_descriptor.pb.dart' as $3;
import 'track_event/track_event.pb.dart' as $1;
@@ -37,6 +38,7 @@ enum TracePacket_Data {
clockSnapshot,
trackEvent,
trackDescriptor,
moduleSymbols,
perfSample,
notSet
}
@@ -77,7 +79,8 @@ class TracePacket extends $pb.GeneratedMessage {
$core.int? sequenceFlags,
$core.int? timestampClockId,
$3.TrackDescriptor? trackDescriptor,
$4.PerfSample? perfSample,
$4.ModuleSymbols? moduleSymbols,
$5.PerfSample? perfSample,
}) {
final result = create();
if (clockSnapshot != null) result.clockSnapshot = clockSnapshot;
@@ -89,6 +92,7 @@ class TracePacket extends $pb.GeneratedMessage {
if (sequenceFlags != null) result.sequenceFlags = sequenceFlags;
if (timestampClockId != null) result.timestampClockId = timestampClockId;
if (trackDescriptor != null) result.trackDescriptor = trackDescriptor;
if (moduleSymbols != null) result.moduleSymbols = moduleSymbols;
if (perfSample != null) result.perfSample = perfSample;
return result;
}
@@ -106,6 +110,7 @@ class TracePacket extends $pb.GeneratedMessage {
6: TracePacket_Data.clockSnapshot,
11: TracePacket_Data.trackEvent,
60: TracePacket_Data.trackDescriptor,
61: TracePacket_Data.moduleSymbols,
66: TracePacket_Data.perfSample,
0: TracePacket_Data.notSet
};
@@ -119,7 +124,7 @@ class TracePacket extends $pb.GeneratedMessage {
package:
const $pb.PackageName(_omitMessageNames ? '' : 'perfetto.protos'),
createEmptyInstance: create)
..oo(0, [6, 11, 60, 66])
..oo(0, [6, 11, 60, 61, 66])
..oo(1, [10])
..aOM<$0.ClockSnapshot>(6, _omitFieldNames ? '' : 'clockSnapshot',
subBuilder: $0.ClockSnapshot.create)
@@ -138,8 +143,10 @@ class TracePacket extends $pb.GeneratedMessage {
fieldType: $pb.PbFieldType.OU3)
..aOM<$3.TrackDescriptor>(60, _omitFieldNames ? '' : 'trackDescriptor',
subBuilder: $3.TrackDescriptor.create)
..aOM<$4.PerfSample>(66, _omitFieldNames ? '' : 'perfSample',
subBuilder: $4.PerfSample.create)
..aOM<$4.ModuleSymbols>(61, _omitFieldNames ? '' : 'moduleSymbols',
subBuilder: $4.ModuleSymbols.create)
..aOM<$5.PerfSample>(66, _omitFieldNames ? '' : 'perfSample',
subBuilder: $5.PerfSample.create)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@@ -164,11 +171,13 @@ class TracePacket extends $pb.GeneratedMessage {
@$pb.TagNumber(6)
@$pb.TagNumber(11)
@$pb.TagNumber(60)
@$pb.TagNumber(61)
@$pb.TagNumber(66)
TracePacket_Data whichData() => _TracePacket_DataByTag[$_whichOneof(0)]!;
@$pb.TagNumber(6)
@$pb.TagNumber(11)
@$pb.TagNumber(60)
@$pb.TagNumber(61)
@$pb.TagNumber(66)
void clearData() => $_clearField($_whichOneof(0));
@@ -276,16 +285,28 @@ class TracePacket extends $pb.GeneratedMessage {
@$pb.TagNumber(60)
$3.TrackDescriptor ensureTrackDescriptor() => $_ensure(7);
/// Only used in profile packets.
@$pb.TagNumber(61)
$4.ModuleSymbols get moduleSymbols => $_getN(8);
@$pb.TagNumber(61)
set moduleSymbols($4.ModuleSymbols value) => $_setField(61, value);
@$pb.TagNumber(61)
$core.bool hasModuleSymbols() => $_has(8);
@$pb.TagNumber(61)
void clearModuleSymbols() => $_clearField(61);
@$pb.TagNumber(61)
$4.ModuleSymbols ensureModuleSymbols() => $_ensure(8);
@$pb.TagNumber(66)
$4.PerfSample get perfSample => $_getN(8);
$5.PerfSample get perfSample => $_getN(9);
@$pb.TagNumber(66)
set perfSample($4.PerfSample value) => $_setField(66, value);
set perfSample($5.PerfSample value) => $_setField(66, value);
@$pb.TagNumber(66)
$core.bool hasPerfSample() => $_has(8);
$core.bool hasPerfSample() => $_has(9);
@$pb.TagNumber(66)
void clearPerfSample() => $_clearField(66);
@$pb.TagNumber(66)
$4.PerfSample ensurePerfSample() => $_ensure(8);
$5.PerfSample ensurePerfSample() => $_ensure(9);
}
const $core.bool _omitFieldNames =
@@ -62,6 +62,15 @@ const TracePacket$json = {
'9': 0,
'10': 'trackDescriptor'
},
{
'1': 'module_symbols',
'3': 61,
'4': 1,
'5': 11,
'6': '.perfetto.protos.ModuleSymbols',
'9': 0,
'10': 'moduleSymbols'
},
{
'1': 'perf_sample',
'3': 66,
@@ -113,11 +122,12 @@ final $typed_data.Uint8List tracePacketDescriptor = $convert.base64Decode(
'KAsyHi5wZXJmZXR0by5wcm90b3MuQ2xvY2tTbmFwc2hvdEgAUg1jbG9ja1NuYXBzaG90Ej4KC3'
'RyYWNrX2V2ZW50GAsgASgLMhsucGVyZmV0dG8ucHJvdG9zLlRyYWNrRXZlbnRIAFIKdHJhY2tF'
'dmVudBJNChB0cmFja19kZXNjcmlwdG9yGDwgASgLMiAucGVyZmV0dG8ucHJvdG9zLlRyYWNrRG'
'VzY3JpcHRvckgAUg90cmFja0Rlc2NyaXB0b3ISPgoLcGVyZl9zYW1wbGUYQiABKAsyGy5wZXJm'
'ZXR0by5wcm90b3MuUGVyZlNhbXBsZUgAUgpwZXJmU2FtcGxlEj0KGnRydXN0ZWRfcGFja2V0X3'
'NlcXVlbmNlX2lkGAogASgNSAFSF3RydXN0ZWRQYWNrZXRTZXF1ZW5jZUlkEkIKDWludGVybmVk'
'X2RhdGEYDCABKAsyHS5wZXJmZXR0by5wcm90b3MuSW50ZXJuZWREYXRhUgxpbnRlcm5lZERhdG'
'ESJQoOc2VxdWVuY2VfZmxhZ3MYDSABKA1SDXNlcXVlbmNlRmxhZ3MiaAoNU2VxdWVuY2VGbGFn'
'cxITCg9TRVFfVU5TUEVDSUZJRUQQABIhCh1TRVFfSU5DUkVNRU5UQUxfU1RBVEVfQ0xFQVJFRB'
'ABEh8KG1NFUV9ORUVEU19JTkNSRU1FTlRBTF9TVEFURRACQgYKBGRhdGFCJQojb3B0aW9uYWxf'
'dHJ1c3RlZF9wYWNrZXRfc2VxdWVuY2VfaWQ=');
'VzY3JpcHRvckgAUg90cmFja0Rlc2NyaXB0b3ISRwoObW9kdWxlX3N5bWJvbHMYPSABKAsyHi5w'
'ZXJmZXR0by5wcm90b3MuTW9kdWxlU3ltYm9sc0gAUg1tb2R1bGVTeW1ib2xzEj4KC3BlcmZfc2'
'FtcGxlGEIgASgLMhsucGVyZmV0dG8ucHJvdG9zLlBlcmZTYW1wbGVIAFIKcGVyZlNhbXBsZRI9'
'Chp0cnVzdGVkX3BhY2tldF9zZXF1ZW5jZV9pZBgKIAEoDUgBUhd0cnVzdGVkUGFja2V0U2VxdW'
'VuY2VJZBJCCg1pbnRlcm5lZF9kYXRhGAwgASgLMh0ucGVyZmV0dG8ucHJvdG9zLkludGVybmVk'
'RGF0YVIMaW50ZXJuZWREYXRhEiUKDnNlcXVlbmNlX2ZsYWdzGA0gASgNUg1zZXF1ZW5jZUZsYW'
'dzImgKDVNlcXVlbmNlRmxhZ3MSEwoPU0VRX1VOU1BFQ0lGSUVEEAASIQodU0VRX0lOQ1JFTUVO'
'VEFMX1NUQVRFX0NMRUFSRUQQARIfChtTRVFfTkVFRFNfSU5DUkVNRU5UQUxfU1RBVEUQAkIGCg'
'RkYXRhQiUKI29wdGlvbmFsX3RydXN0ZWRfcGFja2V0X3NlcXVlbmNlX2lk');
+5
View File
@@ -248,6 +248,11 @@ DEFINE_NATIVE_ENTRY(Developer_NativeRuntime_streamTimelineTo, 0, 5) {
Profiler::SetConfig({
.enabled = true,
.period_us = static_cast<intptr_t>(sampling_interval.Value()),
#if defined(SUPPORT_PERFETTO)
// We only implement profile streaming for perfetto format, we
// assume that the caller ensured that recorder is "perfettofile".
.stream_to_timeline = true,
#endif
});
}
#endif
@@ -14,6 +14,7 @@ import 'package:expect/expect.dart';
import 'package:path/path.dart' as path;
import 'package:vm_service_protos/vm_service_protos.dart';
import '../../../../pkg/perf_witness/test/common/test_utils.dart';
import 'use_flag_test_helper.dart';
@pragma('vm:never-inline')
@@ -55,64 +56,28 @@ Future<void> testPerfettoRecorder({
'$perfettoTimeline does not exist',
);
final trace = Trace()..mergeFromBuffer(perfettoTimeline.readAsBytesSync());
Expect.isNotEmpty(trace.packet);
var state = IncrementalState();
final seenEvents = <String>{};
final seenStacks = <List<String>>{};
final seenTracks = <int>{};
final seenTrackDescriptors = <int>{};
for (var packet in trace.packet) {
if ((packet.sequenceFlags &
TracePacket_SequenceFlags.SEQ_INCREMENTAL_STATE_CLEARED.value) !=
0) {
state = IncrementalState();
}
if (packet.hasInternedData()) {
state.update(packet.internedData);
}
if (packet.hasTrackEvent()) {
final trackEvent = packet.trackEvent;
if (trackEvent.type == TrackEvent_Type.TYPE_SLICE_BEGIN) {
final name = state.eventNames[packet.trackEvent.nameIid.toInt()]!;
seenEvents.add(name);
seenTracks.add(trackEvent.trackUuid.toInt());
}
}
if (packet.hasTrackDescriptor()) {
final trackDescriptor = packet.trackDescriptor;
seenTrackDescriptors.add(trackDescriptor.uuid.toInt());
}
if (packet.hasPerfSample()) {
seenStacks.add(state.stacks[packet.perfSample.callstackIid.toInt()]!);
}
}
final traceData = TraceData.fromBytes(perfettoTimeline.readAsBytesSync());
Expect.isTrue(
seenEvents.containsAll(['workload-loop', 'CollectNewGeneration']),
traceData.seenEvents.containsAll(['workload-loop', 'CollectNewGeneration']),
);
Expect.isTrue(seenTrackDescriptors.containsAll(seenTracks), '''
Expect.isTrue(
traceData.seenTrackDescriptors.containsAll(traceData.seenTracks),
'''
expected to see a track descriptor for every track:
seen descriptors ${seenTrackDescriptors}
seen tracks ${seenTracks}
missing descriptors ${seenTracks.difference(seenTrackDescriptors)}
''');
seen descriptors ${traceData.seenTrackDescriptors}
seen tracks ${traceData.seenTracks}
missing descriptors ${traceData.seenTracks.difference(traceData.seenTrackDescriptors)}
''',
);
if (withProfiler) {
Expect.isNotNull(
seenStacks.firstWhereOrNull(
(stack) =>
stackMatches(stack, ['main', 'workload', 'Timeline.timeSync']),
),
Expect.isTrue(
traceData.hasSeenStack(['main', 'workload', 'Timeline.timeSync']),
);
} else {
Expect.isEmpty(seenStacks);
Expect.isEmpty(traceData.seenStacks);
}
}
@@ -179,45 +144,3 @@ void main() async {
Expect.isFalse(File('whatever').existsSync());
});
}
bool stackMatches(List<String> stack, List<String> expected) {
var i = 0;
var j = 0;
while (j < expected.length) {
while (i < stack.length && stack[i] != expected[j]) {
i++;
}
if (i == stack.length) {
return false;
}
j++;
}
return true;
}
class IncrementalState {
final eventNames = <int, String>{};
final functionNames = <int, String>{};
final frames = <int, String>{};
final stacks = <int, List<String>>{};
void update(InternedData internedData) {
for (var eventName in internedData.eventNames) {
eventNames[eventName.iid.toInt()] = eventName.name;
}
for (var functionName in internedData.functionNames) {
functionNames[functionName.iid.toInt()] = utf8.decode(functionName.str);
}
for (var frame in internedData.frames) {
frames[frame.iid.toInt()] = functionNames[frame.functionNameId.toInt()]!;
}
for (var stack in internedData.callstacks) {
stacks[stack.iid.toInt()] = stack.frameIds
.map((iid) => frames[iid.toInt()]!)
.toList(growable: false);
}
}
}
+8
View File
@@ -687,6 +687,14 @@ char* Dart::Cleanup() {
Profiler::SetConfig({.enabled = false});
#endif // defined(DART_INCLUDE_PROFILER)
#if defined(SUPPORT_TIMELINE)
if (FLAG_trace_shutdown) {
OS::PrintErr("[+%" Pd64 "ms] SHUTDOWN: Stopping timeline streaming\n",
UptimeMillis());
}
Timeline::StopStreaming(/*reinitialize=*/false);
#endif
NativeSymbolResolver::Cleanup();
// Disable the creation of new isolates.
+10 -1
View File
@@ -365,7 +365,16 @@ class ZoneDirectChainedHashMap
DISALLOW_COPY_AND_ASSIGN(ZoneDirectChainedHashMap);
};
// Concept for checking if T provides Hash and Equals methods that are expected
// by |PointerSetKeyValueTrait|.
template <typename T>
concept DefinesHashAndEquality = requires(const T& a, const T& b) {
{ a.Equals(b) } -> std::same_as<bool>;
{ a.Hash() } -> std::same_as<uword>;
};
template <DefinesHashAndEquality T>
class PointerSetKeyValueTrait {
public:
typedef T* Value;
@@ -378,7 +387,7 @@ class PointerSetKeyValueTrait {
static inline bool IsKeyEqual(Pair kv, Key key) { return kv->Equals(*key); }
};
template <typename T>
template <DefinesHashAndEquality T>
using PointerSet = DirectChainedHashMap<PointerSetKeyValueTrait<T>>;
template <typename T>
+1 -1
View File
@@ -14,7 +14,7 @@ class TestValue {
// FinalizeHash is used here to provide coverage for FinalizeHash(...)
// function.
uword Hash() const { return FinalizeHash(static_cast<uint32_t>(x_) & 1); }
bool Equals(const TestValue& other) { return x_ == other.x_; }
bool Equals(const TestValue& other) const { return x_ == other.x_; }
private:
intptr_t x_;
+8
View File
@@ -224,6 +224,14 @@ class PageSpace {
return size >> kWordSizeLog2;
}
template <typename F>
void ForEachImagePage(F&& callback) const {
MutexLocker ml(&pages_lock_);
for (Page* page = image_pages_; page != nullptr; page = page->next()) {
callback(page);
}
}
bool Contains(uword addr) const;
bool ContainsUnsafe(uword addr) const;
bool CodeContains(uword addr) const;
+7 -1
View File
@@ -2612,7 +2612,10 @@ void Isolate::Shutdown() {
#if !defined(PRODUCT)
HandleScope handle_scope(thread);
debugger()->Shutdown();
Profiler::IsolateShutdown(thread);
#endif
#if defined(DART_INCLUDE_PROFILER)
Profiler::IsolateShutdown(this);
#endif
}
@@ -2690,6 +2693,9 @@ void Isolate::LowLevelCleanup(Isolate* isolate) {
const bool shutdown_group = isolate_group->UnregisterIsolateDecrementCount();
if (shutdown_group) {
KernelIsolate::NotifyAboutIsolateGroupShutdown(isolate_group);
#if defined(DART_INCLUDE_PROFILER)
Profiler::IsolateGroupShutdown(isolate_group);
#endif
if (!is_vm_isolate) {
Thread::EnterIsolateGroupAsHelper(isolate_group, Thread::kUnknownTask,
+214 -66
View File
@@ -132,32 +132,78 @@ inline void AppendPacketToJSONBase64String(
});
}
// Sequence of elements which can be interned by |BytesInterner|.
// Sequence of |length| elements of type |T|.
//
// Equality and hash are defined in terms of raw byte content.
// These elements are treated as raw bytes for the purpose of equality and
// hashing.
template <typename T>
struct InternedBytes {
InternedBytes(const T* data, intptr_t length)
: data(data),
length(length),
hash(HashBytes(reinterpret_cast<const uint8_t*>(data),
length * sizeof(T))),
iid(0) {}
struct Span {
const T* const data;
intptr_t length;
InternedBytes(const T* data, intptr_t length, uword hash, uint64_t iid)
: data(data), length(length), hash(hash), iid(iid) {}
template <typename Allocator>
Span<T> Copy(Allocator* allocator) const {
T* copy = allocator->template Alloc<T>(length);
memcpy(copy, data, length * sizeof(T)); // NOLINT
return {copy, length};
}
bool Equals(const InternedBytes& other) const {
template <typename Allocator>
void Dispose(Allocator* allocator) const {
if constexpr (Allocator::kSupportsFreeingIndividualAllocations) {
allocator->Free(const_cast<T*>(data), length);
}
}
bool Equals(const Span& other) const {
if (length != other.length) {
return false;
}
return memcmp(data, other.data, length * sizeof(T)) == 0;
}
uword Hash() const {
return HashBytes(reinterpret_cast<const uint8_t*>(data),
length * sizeof(T));
}
};
template <typename T, typename Allocator>
concept DefinesCopyAndDispose = requires(const T& a, Allocator* allocator) {
{ a.Copy(allocator) } -> std::same_as<T>;
{ a.Dispose(allocator) } -> std::same_as<void>;
};
// Sequence of elements which can be interned by |BytesInterner|.
//
// Equality and hash are defined in terms of raw byte content.
template <typename T>
struct Interned {
explicit Interned(const T& data)
: data(data), hash(ComputeHash(data)), iid(0) {}
Interned(const T& data, uword hash, uint64_t iid)
: data(data), hash(hash), iid(iid) {}
bool Equals(const Interned& other) const {
if constexpr (DefinesHashAndEquality<T>) {
return data.Equals(other.data);
} else {
return memcmp(&data, &other.data, sizeof(T)) == 0;
}
}
static uword ComputeHash(const T& data) {
if constexpr (DefinesHashAndEquality<T>) {
return data.Hash();
} else {
return HashBytes(reinterpret_cast<const uint8_t*>(&data), sizeof(T));
}
}
uword Hash() const { return hash; }
const T* const data;
const intptr_t length;
const T data;
const uword hash;
// Interning id. Only set after interning and does not participate in
@@ -172,19 +218,18 @@ typedef uint8_t InternerStateBits;
// Interning dictionary used to construct various parts of |InternedData|
// message.
template <typename T, typename Allocator>
class BytesInterner
: public BaseDirectChainedHashMap<PointerSetKeyValueTrait<InternedBytes<T>>,
class Interner
: public BaseDirectChainedHashMap<PointerSetKeyValueTrait<Interned<T>>,
ValueObject,
Allocator> {
using Base =
BaseDirectChainedHashMap<PointerSetKeyValueTrait<InternedBytes<T>>,
ValueObject,
Allocator>;
using Base = BaseDirectChainedHashMap<PointerSetKeyValueTrait<Interned<T>>,
ValueObject,
Allocator>;
public:
explicit BytesInterner(Allocator* allocator = nullptr) : Base(allocator) {}
explicit Interner(Allocator* allocator = nullptr) : Base(allocator) {}
~BytesInterner() {
~Interner() {
if constexpr (Allocator::kSupportsFreeingIndividualAllocations) {
auto it = Base::GetIterator();
while (auto pair = it.Next()) {
@@ -193,10 +238,18 @@ class BytesInterner
}
}
uint64_t Intern(const T* data, const intptr_t length) {
uint64_t Lookup(const T& data) {
Interned<T> key(data);
if (auto interned = Base::Lookup(&key)) {
return (*interned)->iid;
}
return 0;
}
uint64_t Intern(const T& data) {
state_ |= kInternerWasUsed;
InternedBytes<T> key(data, length);
Interned<T> key(data);
if (auto interned = Base::Lookup(&key)) {
return (*interned)->iid;
}
@@ -214,7 +267,8 @@ class BytesInterner
// Note: we never remove elements from this map so we can just iterate
// |pairs_| linearly.
for (uint32_t i = first_to_flush_; i < Base::next_pair_index_; i++) {
callback(*Base::pairs_[i]);
auto pair = Base::pairs_[i];
callback(pair->iid, pair->data);
}
first_to_flush_ = Base::next_pair_index_;
}
@@ -225,21 +279,35 @@ class BytesInterner
return result;
}
Interned<T>** begin() { return &Base::pairs_[0]; }
Interned<T>** end() { return &Base::pairs_[Base::next_pair_index_]; }
const Interned<T>** begin() const { return &Base::pairs_[0]; }
const Interned<T>** end() const {
return &Base::pairs_[Base::next_pair_index_];
}
const T& GetByIid(uint64_t iid) const { return Base::pairs_[iid - 1]->data; }
private:
Allocator* allocator() const { return Base::allocator_; }
InternedBytes<T>* Copy(const InternedBytes<T>& interned, uint64_t iid) const {
auto data_copy = allocator()->template Alloc<T>(interned.length);
memcpy(data_copy, interned.data, interned.length * sizeof(T)); // NOLINT
auto copy = allocator()->template Alloc<InternedBytes<T>>(1);
new (copy) InternedBytes<T>(data_copy, interned.length, interned.hash, iid);
Interned<T>* Copy(const Interned<T>& interned, uint64_t iid) const {
auto copy = allocator()->template Alloc<Interned<T>>(1);
if constexpr (DefinesCopyAndDispose<T, Allocator>) {
new (copy)
Interned<T>(interned.data.Copy(allocator()), interned.hash, iid);
} else {
new (copy) Interned<T>(interned.data, interned.hash, iid);
}
return copy;
}
void Dispose(InternedBytes<T>* interned) {
void Dispose(Interned<T>* interned) {
if constexpr (Allocator::kSupportsFreeingIndividualAllocations) {
allocator()->Free(const_cast<T*>(interned->data),
interned->length * sizeof(T));
if constexpr (DefinesCopyAndDispose<T, Allocator>) {
interned->data.Dispose(allocator());
}
allocator()->Free(interned, 1);
}
}
@@ -258,9 +326,15 @@ class StringInterner : public ValueObject {
explicit StringInterner(Allocator* allocator = nullptr)
: bytes_interner_(allocator) {}
uint64_t Lookup(const char* str) {
return bytes_interner_.Lookup(
{str, static_cast<intptr_t>(strlen(str) + 1)});
}
uint64_t Intern(const char* str) {
// +1 to include terminating NUL character.
return bytes_interner_.Intern(str, strlen(str) + 1);
return bytes_interner_.Intern(
{str, static_cast<intptr_t>(strlen(str) + 1)});
}
InternerStateBits TakeAndResetState() {
@@ -270,13 +344,23 @@ class StringInterner : public ValueObject {
template <typename F>
void FlushNewlyInternedTo(F&& callback) {
bytes_interner_.FlushNewlyInternedTo(
[callback = std::move(callback)](const auto& interned_bytes) {
callback(interned_bytes.iid, interned_bytes.data);
[callback = std::move(callback)](auto iid, const auto& span) {
callback(iid, span.data);
});
}
const char* GetByIid(uint64_t iid) const {
return bytes_interner_.GetByIid(iid).data;
}
Interned<Span<char>>** begin() { return bytes_interner_.begin(); }
Interned<Span<char>>** end() { return bytes_interner_.end(); }
const Interned<Span<char>>** begin() const { return bytes_interner_.begin(); }
const Interned<Span<char>>** end() const { return bytes_interner_.end(); }
private:
BytesInterner<char, Allocator> bytes_interner_;
Interner<Span<char>, Allocator> bytes_interner_;
};
// Trait used to map 64-bit ids (e.g. isolate or isolate group id) to
@@ -311,6 +395,50 @@ class InternedDataBuilder : public ValueObject {
enum class UnknownMappingState { kNotNeeded, kNeeded, kEmitted };
public:
struct Mapping {
uint64_t start;
uint64_t end;
uint64_t offset;
uint64_t path_string;
uint64_t build_id;
};
// Each frame is either eagerly symbolized or not. For eagerly symbolized
// frames rel_pc is set to kEagerlySymbolizedFramePc and function_name_iid
// is set to the iid of the function name. For non-eagerly symbolized frames
// rel_pc is set to the relative pc and function_name_iid might or might
// not be set.
//
// We assume that depending on the writer all frames are either eagerly
// symbolized or not.
struct Frame {
static constexpr uint64_t kEagerlySymbolizedFramePc = kMaxUint64;
uint64_t rel_pc = kEagerlySymbolizedFramePc;
uint32_t mapping_iid = 0;
uint32_t function_name_iid = 0;
bool Equals(const Frame& other) const {
// We assume symbolization mode is consistent: either all frames
// have rel_pc set to kEagerlySymbolizedFramePc or none of them do.
if (rel_pc == kEagerlySymbolizedFramePc) {
return mapping_iid == other.mapping_iid &&
function_name_iid == other.function_name_iid;
}
return mapping_iid == other.mapping_iid && rel_pc == other.rel_pc;
}
uword Hash() const {
if (rel_pc == kEagerlySymbolizedFramePc) {
return CombineHashes(Utils::WordHash(mapping_iid),
Utils::WordHash(function_name_iid));
} else {
return CombineHashes(Utils::WordHash(mapping_iid),
Utils::WordHash(rel_pc));
}
}
};
// InternedData contains multiple independent interning dictionaries which
// are used for different attributes.
#define PERFETTO_INTERNED_STRINGS_FIELDS_LIST(V) \
@@ -319,12 +447,13 @@ class InternedDataBuilder : public ValueObject {
V(debug_annotation_names, name) \
V(debug_annotation_string_values, str) \
V(function_names, str) \
V(mapping_paths, str)
V(mapping_paths, str) \
V(build_ids, str)
#define PERFETTO_INTERNED_RAW_BYTES_FIELDS_LIST(V) \
V(callstacks, uint64_t) \
V(mappings, uint64_t) \
V(frames, uint64_t)
#define PERFETTO_INTERNED_FIELDS_LIST(V) \
V(callstacks, Span<uint64_t>) \
V(mappings, Mapping) \
V(frames, Frame)
// Direct access for known strings.
#define PERFETTO_COMMON_INTERNED_STRINGS_LIST(V) \
@@ -372,13 +501,14 @@ class InternedDataBuilder : public ValueObject {
PERFETTO_INTERNED_STRINGS_FIELDS_LIST(FLUSH_FIELD)
#undef FLUSH_FIELD
callstacks_.FlushNewlyInternedTo([interned_data](const auto& interned) {
auto callstack = interned_data->add_callstacks();
callstack->set_iid(interned.iid);
for (intptr_t i = 0; i < interned.length; i++) {
callstack->add_frame_ids(interned.data[i]);
}
});
callstacks_.FlushNewlyInternedTo(
[interned_data](const auto iid, const auto& stack) {
auto callstack = interned_data->add_callstacks();
callstack->set_iid(iid);
for (intptr_t i = 0; i < stack.length; i++) {
callstack->add_frame_ids(stack.data[i]);
}
});
// Perfetto proto message definition claim that mapping iid 0 means
// the same as frame not having mapping information. However Perfetto UI
@@ -392,18 +522,31 @@ class InternedDataBuilder : public ValueObject {
unknown_mapping_ = UnknownMappingState::kEmitted;
}
mappings_.FlushNewlyInternedTo([interned_data](const auto& interned) {
auto mapping = interned_data->add_mappings();
mapping->set_iid(interned.iid);
mapping->add_path_string_ids(interned.data[0]);
});
mappings_.FlushNewlyInternedTo(
[interned_data](const auto iid, const auto& data) {
auto mapping = interned_data->add_mappings();
mapping->set_iid(iid);
mapping->set_start(data.start);
mapping->set_end(data.end);
mapping->set_start_offset(data.offset);
mapping->add_path_string_ids(data.path_string);
if (data.build_id != 0) {
mapping->set_build_id(data.build_id);
}
});
frames_.FlushNewlyInternedTo([interned_data](const auto& interned) {
frames_.FlushNewlyInternedTo([interned_data](const auto iid,
const auto& data) {
auto frame = interned_data->add_frames();
frame->set_iid(interned.iid);
frame->set_function_name_id(interned.data[0]);
if (interned.data[1] != 0) {
frame->set_mapping_id(interned.data[1]);
frame->set_iid(iid);
if (data.function_name_iid != 0) {
frame->set_function_name_id(data.function_name_iid);
}
if (data.mapping_iid != 0) {
frame->set_mapping_id(data.mapping_iid);
}
if (data.rel_pc != 0 && data.rel_pc != Frame::kEagerlySymbolizedFramePc) {
frame->set_rel_pc(data.rel_pc);
}
});
}
@@ -414,10 +557,8 @@ class InternedDataBuilder : public ValueObject {
#undef DEFINE_GETTER
#define DEFINE_GETTER(name, element_type) \
perfetto_utils::BytesInterner<element_type, Malloc>& name() { \
return name##_; \
}
PERFETTO_INTERNED_RAW_BYTES_FIELDS_LIST(DEFINE_GETTER)
perfetto_utils::Interner<element_type, Malloc>& name() { return name##_; }
PERFETTO_INTERNED_FIELDS_LIST(DEFINE_GETTER)
#undef DEFINE_GETTER
#define DEFINE_GETTER_FOR_COMMON_STRING(category, str) \
@@ -444,6 +585,13 @@ class InternedDataBuilder : public ValueObject {
ISOLATE_GROUP_SERVICE_ID_FORMAT_STRING, isolate_group_id);
}
uint64_t InternSyntheticBuildIdForIsolateGroup(Dart_Port isolate_group_id) {
char build_id_string[3 + sizeof(Dart_Port) * 2 + 1];
Utils::SNPrint(build_id_string, ARRAY_SIZE(build_id_string),
"ig/%016" Px64 "", isolate_group_id);
return build_ids().Intern(build_id_string);
}
private:
template <std::size_t kFormatLen>
uint64_t InternFormattedIdForDebugAnnotation(IdToIidMap& cache,
@@ -469,7 +617,7 @@ class InternedDataBuilder : public ValueObject {
#define TAKE_AND_RESET(name, ignored) result |= name##_.TakeAndResetState();
PERFETTO_INTERNED_STRINGS_FIELDS_LIST(TAKE_AND_RESET)
PERFETTO_INTERNED_RAW_BYTES_FIELDS_LIST(TAKE_AND_RESET)
PERFETTO_INTERNED_FIELDS_LIST(TAKE_AND_RESET)
#undef TAKE_AND_RESET
return result;
@@ -496,8 +644,8 @@ class InternedDataBuilder : public ValueObject {
#undef DEFINE_FIELD
#define DEFINE_FIELD(name, element_type) \
perfetto_utils::BytesInterner<element_type, Malloc> name##_;
PERFETTO_INTERNED_RAW_BYTES_FIELDS_LIST(DEFINE_FIELD)
perfetto_utils::Interner<element_type, Malloc> name##_;
PERFETTO_INTERNED_FIELDS_LIST(DEFINE_FIELD)
#undef DEFINE_FIELD
DISALLOW_COPY_AND_ASSIGN(InternedDataBuilder);
+393 -58
View File
@@ -4,11 +4,16 @@
#include "vm/profiler.h"
#include <utility>
#include "platform/address_sanitizer.h"
#include "platform/atomic.h"
#include "platform/memory_sanitizer.h"
#include "platform/thread_sanitizer.h"
#include "platform/utils.h"
#if defined(SUPPORT_PERFETTO)
#include "third_party/perfetto/protos/perfetto/trace/profiling/profile_packet.pbzero.h"
#endif
#include "vm/allocation.h"
#include "vm/code_patcher.h"
#if !defined(DART_PRECOMPILED_RUNTIME)
@@ -17,6 +22,9 @@
#include "vm/debugger.h"
#include "vm/globals.h"
#include "vm/heap/safepoint.h"
#if defined(DART_PRECOMPILED_RUNTIME)
#include "vm/image_snapshot.h"
#endif
#include "vm/instructions.h"
#include "vm/isolate.h"
#include "vm/json_stream.h"
@@ -26,6 +34,9 @@
#include "vm/object.h"
#include "vm/object_store.h"
#include "vm/os.h"
#if defined(SUPPORT_PERFETTO)
#include "vm/perfetto_utils.h"
#endif
#include "vm/profiler_service.h"
#include "vm/reusable_handles.h"
#include "vm/signal_handler.h"
@@ -629,9 +640,8 @@ Profiler::Config Profiler::config_ = {.enabled = false,
.max_depth = 0};
RelaxedAtomic<bool> Profiler::running_ = false;
SampleBlockBuffer* Profiler::sample_block_buffer_ = nullptr;
Profiler::ProfileProcessorCallback Profiler::process_profile_callback_ =
nullptr;
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
bool SampleBlockProcessor::initialized_ = false;
bool SampleBlockProcessor::shutdown_ = false;
bool SampleBlockProcessor::drain_ = false;
@@ -639,11 +649,14 @@ bool SampleBlockProcessor::thread_running_ = false;
ThreadJoinId SampleBlockProcessor::processor_thread_id_ =
OSThread::kInvalidThreadJoinId;
Monitor* SampleBlockProcessor::monitor_ = nullptr;
#endif
void Profiler::Init() {
monitor_ = new Monitor();
ThreadInterrupter::Init();
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
SampleBlockProcessor::Init();
#endif
SetConfig({});
}
@@ -653,7 +666,9 @@ void Profiler::Cleanup() {
StopLocked();
}
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
SampleBlockProcessor::Cleanup();
#endif
ThreadInterrupter::Cleanup();
delete monitor_;
}
@@ -669,6 +684,9 @@ Profiler::Config NormalizeConfig(const Profiler::Config& config) {
.max_depth = Utils::Minimum(
kMaximumDepth,
Utils::Maximum(kMinimumDepth, config.max_depth.load())),
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
.stream_to_timeline = config.stream_to_timeline,
#endif
};
}
} // namespace
@@ -688,6 +706,16 @@ void Profiler::SetConfig(const Profiler::Config& config) {
StopLocked();
}
} else if (old_config.enabled) {
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
if (new_config.stream_to_timeline != old_config.stream_to_timeline) {
if (new_config.stream_to_timeline) {
SampleBlockProcessor::Startup();
} else {
SampleBlockProcessor::Shutdown();
}
}
#endif
// Check if we need to reconfigure a running profiler.
//
// Note: this will not resize the sampling buffer, you
@@ -718,7 +746,11 @@ void Profiler::StartLocked() {
}
ThreadInterrupter::SetInterruptPeriod(config_.period_us);
ThreadInterrupter::Startup();
SampleBlockProcessor::Startup();
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
if (config_.stream_to_timeline) {
SampleBlockProcessor::Startup();
}
#endif
running_ = true;
}
@@ -739,9 +771,9 @@ void Profiler::StopLocked() {
}
ThreadInterrupter::Shutdown();
const bool should_drain = process_profile_callback_ != nullptr;
SampleBlockProcessor::Shutdown(should_drain);
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
SampleBlockProcessor::Shutdown();
#endif
SampleBlockCleanupVisitor visitor;
Isolate::VisitIsolates(&visitor);
@@ -1789,6 +1821,327 @@ ProcessedSampleBuffer* SampleBuffer::BuildProcessedSampleBuffer(
return buffer;
}
#if defined(SUPPORT_PERFETTO) && defined(DART_PRECOMPILED_RUNTIME)
class PerfettoPerfSampleWriter : public ValueObject {
public:
PerfettoPerfSampleWriter(
int64_t from_micros,
int64_t to_micros,
perfetto_utils::InternedDataBuilder& interned_data_builder,
void* file,
Dart_FileWriteCallback write_bytes)
: from_micros_(from_micros),
to_micros_(to_micros),
file_(file),
write_bytes_(write_bytes),
interned_data_builder_(interned_data_builder) {
CollectMappings();
}
~PerfettoPerfSampleWriter() {
for (auto m : mappings_) {
delete m;
}
}
struct SnapshotMapping : public MallocAllocated {
uint32_t iid;
uword start;
uword end;
const char* path;
Dart_Port isolate_group_id;
bool is_root_unit;
bool Contains(uword pc) { return start < pc && pc <= end; }
};
void CollectMappings() {
IsolateGroup::ForEach([&](IsolateGroup* group) {
const auto group_source = group->source();
const auto isolate_group_instructions =
reinterpret_cast<uword>(group_source->snapshot_instructions);
const Image isolate_group_image(isolate_group_instructions);
group->heap()->old_space()->ForEachImagePage([&](Page* page) {
if (page->is_executable()) {
mappings_.Add(new SnapshotMapping{
.start = page->object_start(),
.end = page->object_end(),
.path = group->source()->script_uri,
.isolate_group_id = group->id(),
.is_root_unit =
(page->object_start() ==
reinterpret_cast<uword>(isolate_group_image.object_start())),
});
}
});
});
mappings_.Sort([](auto a, auto b) -> int {
if ((*a)->start < (*b)->start) return -1;
if ((*a)->start > (*b)->start) return 1;
return 0;
});
// Remove duplicated mappings.
intptr_t j = 0;
for (intptr_t i = 0; i < mappings_.length(); i++) {
if (j > 0 && mappings_[j - 1]->start == mappings_[i]->start) {
delete mappings_[i];
} else {
mappings_[j++] = mappings_[i];
}
}
mappings_.SetLength(j);
}
void WriteSamples(SampleBuffer* buffer) {
const intptr_t length = buffer->capacity();
for (intptr_t i = 0; i < length; i++) {
Sample* sample = buffer->At(i);
if (sample->ignore_sample()) {
// Bad sample.
continue;
}
if (!sample->head_sample()) {
// An inner sample in a chain of samples.
continue;
}
if (sample->timestamp() == 0) {
// Empty.
continue;
}
if (sample->At(0) == 0) {
// No frames.
continue;
}
if (sample->is_allocation_sample()) {
continue;
}
auto timestamp = sample->timestamp();
if (from_micros_ > timestamp || to_micros_ < timestamp) {
continue;
}
WriteSample(sample);
}
}
std::pair<uint32_t, uint64_t> FindMapping(uword pc) {
const auto lower_bound =
std::lower_bound(mappings_.begin(), mappings_.end(), pc,
[](auto m, auto pc) { return m->end < pc; });
if (lower_bound == mappings_.end() || !(*lower_bound)->Contains(pc)) {
return std::make_pair(0, pc);
}
const auto m = *lower_bound;
return std::make_pair(InternMapping(m), pc - m->start);
}
uint32_t InternMapping(SnapshotMapping* m) {
if (m->iid == 0) {
// When Perfetto is matching ModuleSymbols to a corresponding mapping,
// it uses both path and build_id for matching (and both of them are
// used as opaque identifiers). We use this to support deferred units:
// all mappings corresponding to an isolate group have the same build-id
// (which is based on isolate group id) while path is based on the script
// uri with address of the mapping appended for non-root units - this
// makes the combination of path+build_id unique for each unit including
// the root one.
//
// Additionally we make sure to prepend "/" to the path if it does not
// start with "/" to compensation for similar logic in Perfetto:
// Mapping.path_string_ids is an array of path components, to construct
// mappings path from path components Perfetto joins them with "/"
// and prepends "/" if there is no leading slash (see [1]). To normalize
// paths between Mapping and ModuleSymbols we simply ensure that path
// here always starts with "/".
//
// [1]: https://github.com/google/perfetto/blob/a3e107ec803c876a870205f89c1e37742184b598/src/trace_processor/importers/proto/profile_packet_utils.cc#L24-L38
const char* path = m->path;
if (!m->is_root_unit) {
Utils::SNPrint(&name_buf_[0], ARRAY_SIZE(name_buf_),
"%s%s(%016" Px64 ")", m->path[0] == '/' ? "" : "/",
m->path, static_cast<uint64_t>(m->start));
path = name_buf_;
} else if (m->path[0] != '/') {
Utils::SNPrint(&name_buf_[0], ARRAY_SIZE(name_buf_), "/%s", m->path);
path = name_buf_;
}
const auto path_id = interned_data_builder_.mapping_paths().Intern(path);
const auto build_id_iid =
interned_data_builder_.InternSyntheticBuildIdForIsolateGroup(
m->isolate_group_id);
m->iid = interned_data_builder_.mappings().Intern({
.start = m->start,
.end = m->end,
.path_string = path_id,
.build_id = build_id_iid,
});
}
return m->iid;
}
void WriteSample(Sample* sample) {
WriteClockSnapshotPacket();
// Walk the sampled PCs and intern the stack.
callstack_.Clear();
Sample* current = sample;
bool unknown_mappings = false;
intptr_t pc_adjustment = 0;
while (current != nullptr) {
for (intptr_t i = 0; i < Sample::kPCArraySizeInWords; i++) {
if (current->At(i) == 0) {
break;
}
const uword pc = current->At(i) + pc_adjustment;
const auto [mapping_iid, rel_pc] = FindMapping(pc);
const auto frame_iid = interned_data_builder_.frames().Intern({
.rel_pc = rel_pc,
.mapping_iid = mapping_iid,
});
if (mapping_iid == 0) {
unknown_mappings = true;
// Eagerly symbolize native frames.
const auto& frame =
interned_data_builder_.frames().GetByIid(frame_iid);
if (frame.function_name_iid == 0) {
const auto name_iid =
interned_data_builder_.function_names().Intern(
LookupNativeName(pc));
const_cast<perfetto_utils::InternedDataBuilder::Frame&>(frame)
.function_name_iid = name_iid;
}
}
callstack_.Add(frame_iid);
pc_adjustment = -1;
}
current = current->Next();
}
if (unknown_mappings) {
interned_data_builder_.MarkNeedUnknownMapping();
}
// Perfetto UI requires callstack frames to be in caller-first order, while
// profiler records samples in callee-first order.
callstack_.Reverse();
const auto callstack_iid = interned_data_builder_.callstacks().Intern(
{&callstack_[0], callstack_.length()});
perfetto_utils::SetTrustedPacketSequenceId(packet_.get());
perfetto_utils::SetTimestampAndMonotonicClockId(packet_.get(),
sample->timestamp());
auto& perf_sample = *packet_->set_perf_sample();
perf_sample.set_pid(pid_);
perf_sample.set_tid(OSThread::ThreadIdToIntPtr(sample->tid()));
perf_sample.set_callstack_iid(callstack_iid);
interned_data_builder_.AttachInternedDataTo(packet_.get());
perfetto_utils::WritePacketBytes(&packet_, [this](auto bytes, auto size) {
write_bytes_(bytes, size, file_);
});
packet_.Reset();
}
private:
void WriteClockSnapshotPacket() {
if (clock_snapshot_written_) {
return;
}
perfetto_utils::PopulateClockSnapshotPacket(packet_.get());
perfetto_utils::WritePacketBytes(&packet_, [this](auto bytes, auto size) {
write_bytes_(bytes, size, file_);
});
packet_.Reset();
clock_snapshot_written_ = true;
}
char* LookupNativeName(uword pc) {
uword start;
if (auto const name = NativeSymbolResolver::LookupSymbolName(pc, &start)) {
Utils::SNPrint(&name_buf_[0], ARRAY_SIZE(name_buf_),
"[Native] %s+0x%" Px "", name, pc - start);
NativeSymbolResolver::FreeSymbolName(name);
return &name_buf_[0];
}
uword dso_base;
const char* dso_name;
if (NativeSymbolResolver::LookupSharedObject(pc, &dso_base, &dso_name)) {
uword dso_offset = pc - dso_base;
Utils::SNPrint(&name_buf_[0], ARRAY_SIZE(name_buf_),
"[Native] %s+0x%" Px "", dso_name, dso_offset);
NativeSymbolResolver::FreeSymbolName(dso_name);
return &name_buf_[0];
} else {
Utils::SNPrint(&name_buf_[0], ARRAY_SIZE(name_buf_), "[Native] %" Px "",
pc);
return &name_buf_[0];
}
}
int64_t from_micros_;
int64_t to_micros_;
void* file_;
Dart_FileWriteCallback write_bytes_;
const intptr_t pid_ = OS::ProcessId();
MallocGrowableArray<SnapshotMapping*> mappings_;
char name_buf_[1024];
perfetto_utils::InternedDataBuilder& interned_data_builder_;
bool clock_snapshot_written_ = false;
protozero::HeapBuffered<perfetto::protos::pbzero::TracePacket> packet_;
MallocGrowableArray<uint64_t> callstack_{128};
};
void SampleBlockBuffer::WritePerfetto(
int64_t from_micros,
int64_t to_micros,
perfetto_utils::InternedDataBuilder& interned_data_builder,
void* file,
Dart_FileWriteCallback write_bytes) {
PerfettoPerfSampleWriter writer(from_micros, to_micros, interned_data_builder,
file, write_bytes);
for (intptr_t i = 0; i < capacity_; ++i) {
SampleBlock* block = &blocks_[i];
if (block->TryAcquireStreaming(/*isolate=*/nullptr)) {
writer.WriteSamples(block);
block->StreamingToFree(); // We consumed samples.
}
}
}
#endif
ProcessedSample* SampleBuffer::BuildProcessedSample(
Sample* sample,
const CodeLookupTable& clt) {
@@ -1811,8 +2164,9 @@ ProcessedSample* SampleBuffer::BuildProcessedSample(
// Copy stack trace from sample(s).
bool truncated = false;
Sample* current = sample;
while (current != nullptr) {
for (Sample* current = sample; current != nullptr;
current = current->Next()) {
for (intptr_t i = 0; i < Sample::kPCArraySizeInWords; i++) {
if (current->At(i) == 0) {
break;
@@ -1821,7 +2175,6 @@ ProcessedSample* SampleBuffer::BuildProcessedSample(
}
truncated = truncated || current->truncated_trace();
current = Next(current);
}
if (!sample->exit_frame_sample()) {
@@ -1833,24 +2186,6 @@ ProcessedSample* SampleBuffer::BuildProcessedSample(
return processed_sample;
}
Sample* SampleBuffer::Next(Sample* sample) {
if (!sample->is_continuation_sample()) return nullptr;
Sample* next_sample = sample->continuation_sample();
// Sanity check.
ASSERT(sample != next_sample);
// Detect invalid chaining.
if (sample->port() != next_sample->port()) {
return nullptr;
}
if (sample->timestamp() != next_sample->timestamp()) {
return nullptr;
}
if (sample->tid() != next_sample->tid()) {
return nullptr;
}
return next_sample;
}
ProcessedSample::ProcessedSample()
: pcs_(Sample::kPCArraySizeInWords),
timestamp_(0),
@@ -1939,6 +2274,7 @@ ProcessedSampleBuffer::ProcessedSampleBuffer()
ASSERT(code_lookup_table_ != nullptr);
}
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
void SampleBlockProcessor::Init() {
ASSERT(!initialized_);
monitor_ = new Monitor();
@@ -1966,16 +2302,14 @@ void SampleBlockProcessor::Startup() {
ASSERT(processor_thread_id_ != OSThread::kInvalidThreadJoinId);
}
void SampleBlockProcessor::Shutdown(bool drain /* = false */) {
void SampleBlockProcessor::Shutdown() {
{
SafepointMonitorLocker shutdown_ml(monitor_);
if (shutdown_) {
// Already shutdown.
return;
}
drain_ = drain;
shutdown_ = true;
// Notify.
shutdown_ml.Notify();
ASSERT(initialized_);
}
@@ -1993,31 +2327,17 @@ void SampleBlockProcessor::Shutdown(bool drain /* = false */) {
ASSERT(!thread_running_);
}
void Profiler::ProcessCompletedBlocks(Isolate* isolate) {
const auto process_profile_callback = process_profile_callback_;
if (process_profile_callback == nullptr) {
return;
}
auto thread = Thread::Current();
if (Isolate::IsSystemIsolate(isolate)) return;
TIMELINE_DURATION(thread, Isolate, "Profiler::ProcessCompletedBlocks")
DisableThreadInterruptsScope dtis(thread);
StackZone zone(thread);
HandleScope handle_scope(thread);
NoAllocationSampleFilter filter(isolate->main_port(), Thread::kMutatorTask,
-1, -1);
Profile profile;
profile.Build(thread, isolate, &filter, Profiler::sample_block_buffer());
process_profile_callback(profile);
void Profiler::IsolateShutdown(Isolate* isolate) {
FlushSampleBlocks(isolate);
NOT_IN_PRECOMPILED(Timeline::DrainCompletedSampleBlocksIntoRecorder(isolate));
}
void Profiler::IsolateShutdown(Thread* thread) {
FlushSampleBlocks(thread->isolate());
ProcessCompletedBlocks(thread->isolate());
void Profiler::IsolateGroupShutdown(IsolateGroup* isolate_group) {
#if defined(SUPPORT_TIMELINE)
if (config_.enabled && config_.stream_to_timeline) {
Timeline::NotifyAboutIsolateGroupShutdown(isolate_group);
}
#endif // defined(SUPPORT_TIMELINE)
}
void SampleBlockProcessor::ThreadMain(uword parameters) {
@@ -2037,10 +2357,23 @@ void SampleBlockProcessor::ThreadMain(uword parameters) {
const int64_t wakeup_interval = 1000 * 100;
while (true) {
wait_ml.WaitMicros(wakeup_interval);
if (shutdown_ && !drain_) {
break;
}
#if defined(DART_PRECOMPILED_RUNTIME)
// If shutting down flush all sample blocks from all isolates.
if (shutdown_) {
IsolateGroup::ForEach([&](IsolateGroup* group) {
if (group == Dart::vm_isolate_group()) return;
const bool kBypassSafepoint = false;
Thread::EnterIsolateGroupAsHelper(group, Thread::kSampleBlockTask,
kBypassSafepoint);
group->ForEachIsolate(
[&](Isolate* isolate) { FlushSampleBlocks(isolate); });
Thread::ExitIsolateGroupAsHelper(kBypassSafepoint);
});
}
Timeline::DrainCompletedSampleBlocksIntoRecorder();
#else
IsolateGroup::ForEach([&](IsolateGroup* group) {
if (group == Dart::vm_isolate_group()) return;
@@ -2048,15 +2381,16 @@ void SampleBlockProcessor::ThreadMain(uword parameters) {
Thread::EnterIsolateGroupAsHelper(group, Thread::kSampleBlockTask,
kBypassSafepoint);
group->ForEachIsolate([&](Isolate* isolate) {
if (drain_) {
if (shutdown_) {
FlushSampleBlocks(isolate);
}
if (isolate->TakeHasCompletedBlocks()) {
Profiler::ProcessCompletedBlocks(isolate);
Timeline::DrainCompletedSampleBlocksIntoRecorder(isolate);
}
});
Thread::ExitIsolateGroupAsHelper(kBypassSafepoint);
});
#endif
if (shutdown_) {
break;
@@ -2065,6 +2399,7 @@ void SampleBlockProcessor::ThreadMain(uword parameters) {
// Signal to main thread we are exiting.
thread_running_ = false;
}
#endif
#endif // defined(DART_INCLUDE_PROFILER)
+42 -17
View File
@@ -27,6 +27,12 @@ class ProcessedSample;
class ProcessedSampleBuffer;
class Profile;
#if defined(SUPPORT_PERFETTO)
namespace perfetto_utils {
class InternedDataBuilder;
} // namespace perfetto_utils
#endif
class Sample;
class SampleBlock;
@@ -68,6 +74,9 @@ class Profiler : public AllStatic {
bool enabled = FLAG_profiler;
intptr_t period_us = FLAG_profile_period;
RelaxedAtomic<intptr_t> max_depth = FLAG_max_profile_depth;
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
bool stream_to_timeline = false;
#endif
};
// Configure the profiler.
@@ -89,12 +98,6 @@ class Profiler : public AllStatic {
#endif // defined(DART_INCLUDE_PROFILER)
}
typedef void (*ProfileProcessorCallback)(Profile&);
static void SetProfileProcessorCallback(ProfileProcessorCallback callback) {
process_profile_callback_ = callback;
}
static SampleBlockBuffer* sample_block_buffer() {
return sample_block_buffer_;
}
@@ -125,11 +128,8 @@ class Profiler : public AllStatic {
}
inline static intptr_t Size();
// This function is currently a no-op, but should not be fully deleted
// because it will be used to implement
// go/dart-universal-observability-for-tools.
static void ProcessCompletedBlocks(Isolate* isolate);
static void IsolateShutdown(Thread* thread);
static void IsolateShutdown(Isolate* thread);
static void IsolateGroupShutdown(IsolateGroup* isolate_group);
private:
// Start the profiler.
@@ -166,8 +166,6 @@ class Profiler : public AllStatic {
static ProfilerCounters counters_;
static ProfileProcessorCallback process_profile_callback_;
friend class Thread;
};
@@ -389,6 +387,25 @@ class Sample {
Sample* continuation_sample() const { return next_; }
Sample* Next() const {
if (!is_continuation_sample()) return nullptr;
Sample* next_sample = continuation_sample();
// Detect invalid chaining.
if (this == next_sample) {
return nullptr;
}
if (port() != next_sample->port()) {
return nullptr;
}
if (timestamp() != next_sample->timestamp()) {
return nullptr;
}
if (tid() != next_sample->tid()) {
return nullptr;
}
return next_sample;
}
intptr_t allocation_cid() const {
ASSERT(is_allocation_sample());
return metadata();
@@ -683,8 +700,6 @@ class SampleBuffer {
ProcessedSampleBuffer* buffer = nullptr);
protected:
Sample* Next(Sample* sample);
ProcessedSample* BuildProcessedSample(Sample* sample,
const CodeLookupTable& clt);
@@ -738,7 +753,7 @@ class SampleBlock : public SampleBuffer {
}
bool TryAcquireStreaming(Isolate* isolate) {
if (state_.load(std::memory_order_relaxed) != kCompleted) return false;
if (owner_ != isolate) return false;
if (isolate != nullptr && owner_ != isolate) return false;
State expected = kCompleted;
State desired = kStreaming;
@@ -828,6 +843,14 @@ class SampleBlockBuffer {
SampleFilter* filter,
ProcessedSampleBuffer* buffer = nullptr);
#if defined(SUPPORT_PERFETTO)
void WritePerfetto(int64_t from_micros,
int64_t to_micros,
perfetto_utils::InternedDataBuilder& interned_data_builder,
void* file,
Dart_FileWriteCallback write_bytes);
#endif
private:
Sample* ReserveSampleImpl(Isolate* isolate, bool allocation_sample);
@@ -964,6 +987,7 @@ class ProcessedSampleBuffer : public ZoneObject {
DISALLOW_COPY_AND_ASSIGN(ProcessedSampleBuffer);
};
#if defined(SUPPORT_TIMELINE) && defined(SUPPORT_PERFETTO)
class SampleBlockProcessor : public AllStatic {
public:
// Initialize the state on VM startup.
@@ -976,7 +1000,7 @@ class SampleBlockProcessor : public AllStatic {
static void Startup();
// Shutdown the worker thread.
static void Shutdown(bool drain = false);
static void Shutdown();
private:
static constexpr intptr_t kMaxThreads = 4096;
@@ -989,6 +1013,7 @@ class SampleBlockProcessor : public AllStatic {
static void ThreadMain(uword parameters);
};
#endif
class NoAllocationSampleFilter : public SampleFilter {
public:
+5 -5
View File
@@ -1893,7 +1893,7 @@ void Profile::PrintSamplesPerfetto(
}
const auto callstack_iid = interned_data_builder.callstacks().Intern(
&callstack[0], callstack.length());
{&callstack[0], callstack.length()});
perfetto_utils::SetTrustedPacketSequenceId(packet.get());
perfetto_utils::SetTimestampAndMonotonicClockId(packet.get(),
@@ -1971,8 +1971,8 @@ void Profile::PrintProfilePerfettoImpl(
interned_data_builder.mapping_paths().Intern(resolved_script_url);
}
mapping_iid = interned_data_builder.mappings().Intern(&mapping_path_iid,
/*length=*/1);
mapping_iid = interned_data_builder.mappings().Intern(
{.path_string = mapping_path_iid});
}
if (mapping_iid == 0) {
@@ -1983,9 +1983,9 @@ void Profile::PrintProfilePerfettoImpl(
// name and source location (through the interned data table). A Perfetto
// |Callstack| consists of a stack of |Frame|s, so the |Callstack|s
// populated by |PrintSamplesPerfetto| will refer to these |Frame|s.
uint64_t frame_info[2] = {function_name_iid, mapping_iid};
function_iids.Add(interned_data_builder.frames().Intern(
frame_info, ARRAY_SIZE(frame_info)));
{.mapping_iid = static_cast<uint32_t>(mapping_iid),
.function_name_iid = static_cast<uint32_t>(function_name_iid)}));
thread->CheckForSafepoint();
}
+196 -23
View File
@@ -22,11 +22,13 @@
#include "platform/atomic.h"
#include "platform/hashmap.h"
#include "vm/image_snapshot.h"
#include "vm/isolate.h"
#include "vm/json_stream.h"
#include "vm/lockers.h"
#include "vm/log.h"
#include "vm/object.h"
#include "vm/reverse_pc_lookup_cache.h"
#include "vm/service.h"
#include "vm/service_event.h"
#include "vm/thread.h"
@@ -515,6 +517,10 @@ Dart_TimelineRecorderCallback Timeline::callback_ = nullptr;
MallocGrowableArray<char*>* Timeline::enabled_streams_ = nullptr;
bool Timeline::recorder_discards_clock_values_ = false;
static std::atomic<bool> is_streaming_timeline{false};
static RelaxedAtomic<uint64_t> streaming_start_micros = 0;
static RelaxedAtomic<uint64_t> streaming_stop_micros = kMaxInt64;
#define TIMELINE_STREAM_DEFINE(name, fuchsia_name, static_labels) \
TimelineStream Timeline::stream_##name##_(#name, fuchsia_name, \
static_labels, false);
@@ -2308,14 +2314,18 @@ class TimelineEventPerfettoFileRecorder
const char* name() const final { return PERFETTO_FILE_RECORDER_NAME; }
#if defined(DART_INCLUDE_PROFILER)
void WriteProfile(Profile& profile);
void WriteProfile(TimelineProfileType& profile) override;
#endif
void NotifyAboutIsolateGroupShutdown(IsolateGroup* isolate_group) override;
private:
void WritePacket(
protozero::HeapBuffered<perfetto::protos::pbzero::TracePacket>* packet);
void DrainImpl(const TimelineEvent& event) final;
void EmitModuleSymbolsFor(IsolateGroup* isolate_group);
Mutex writer_mutex_;
TracePacketWriter writer_;
};
@@ -2327,7 +2337,8 @@ static TimelineEventRecorder* CreateTimelineEventPerfettoFileRecorder(
}
#if defined(DART_INCLUDE_PROFILER)
void TimelineEventPerfettoFileRecorder::WriteProfile(Profile& profile) {
void TimelineEventPerfettoFileRecorder::WriteProfile(
TimelineProfileType& profile) {
// Profile conversion code checks for safepoints so we need to use safepoint
// aware mutex locker here to avoid deadlocks when two threads call
// |WriteProfile| and the third thread requests a safepoint.
@@ -2335,12 +2346,23 @@ void TimelineEventPerfettoFileRecorder::WriteProfile(Profile& profile) {
// Note that Drain does not need this because it does not check for
// safepoint.
SafepointMutexLocker ml(&writer_mutex_);
#if defined(DART_PRECOMPILED_RUNTIME)
profile.WritePerfetto(
streaming_start_micros, streaming_stop_micros,
writer_.interned_data_builder(), this,
[](auto buffer, auto length, auto stream) {
static_cast<TimelineEventPerfettoFileRecorder*>(stream)->Write(
static_cast<const char*>(buffer), length);
});
#else
profile.PrintProfilePerfetto(
writer_.interned_data_builder(), this,
[](auto buffer, auto length, auto stream) {
static_cast<TimelineEventPerfettoFileRecorder*>(stream)->Write(
static_cast<const char*>(buffer), length);
});
#endif
}
#endif
@@ -2366,14 +2388,129 @@ TimelineEventPerfettoFileRecorder::TimelineEventPerfettoFileRecorder(
StartUp("TimelineEventPerfettoFileRecorder");
}
void TimelineEventPerfettoFileRecorder::EmitModuleSymbolsFor(
IsolateGroup* isolate_group) {
auto& interned_data_builder = writer_.interned_data_builder();
const auto build_id_iid =
interned_data_builder.InternSyntheticBuildIdForIsolateGroup(
isolate_group->id());
if (build_id_iid == 0) {
return;
}
const bool need_to_enter_different_group =
IsolateGroup::Current() != isolate_group &&
(Dart::vm_isolate_group() != isolate_group ||
IsolateGroup::Current() == nullptr);
if (need_to_enter_different_group) {
// Exit the current isolate, caller will re-enter it if necessary.
if (Isolate::Current() != nullptr) {
Thread::ExitIsolate();
}
const bool kBypassSafepoint = false;
Thread::EnterIsolateGroupAsHelper(isolate_group, Thread::kUnknownTask,
kBypassSafepoint);
}
StackZone stack_zone(Thread::Current());
Code& code = Code::Handle();
GrowableArray<const Function*> functions;
GrowableArray<TokenPosition> token_positions;
// We assume that in general there is going to be a small number of mappings
// (usually just one for each isolate group) and small number of isolate
// groups (just one) so iterating them linearly is fine.
for (auto interned_mapping : interned_data_builder.mappings()) {
const auto mapping_iid = interned_mapping->iid;
const auto& mapping = interned_mapping->data;
if (mapping.build_id != build_id_iid) {
continue;
}
protozero::HeapBuffered<perfetto::protos::pbzero::TracePacket>& packet =
this->packet();
perfetto::protos::pbzero::ModuleSymbols* module_symbols = nullptr;
for (const auto& interned_frame : interned_data_builder.frames()) {
const auto& frame = interned_frame->data;
if (frame.mapping_iid != mapping_iid) {
continue;
}
const auto pc = mapping.start + frame.rel_pc;
// Note: PCs are already adjusted when converting from Sample to interned
// Frame, see PerfettoPerfSampleWriter::WriteSample.
code = ReversePc::Lookup(isolate_group, pc, /*is_return_address=*/false);
if (code.IsNull()) {
continue;
}
if (module_symbols == nullptr) {
perfetto_utils::SetTrustedPacketSequenceId(packet.get());
module_symbols = packet->set_module_symbols();
const auto mapping_path =
interned_data_builder.mapping_paths().GetByIid(mapping.path_string);
module_symbols->set_path(mapping_path);
module_symbols->set_build_id(
interned_data_builder.build_ids().GetByIid(build_id_iid));
}
auto* address_symbols = module_symbols->add_address_symbols();
address_symbols->set_address(frame.rel_pc);
if (code.IsFunctionCode()) {
const intptr_t offset = pc - code.PayloadStart();
code.GetInlinedFunctionsAtInstruction(offset, &functions,
&token_positions);
for (intptr_t i = functions.length() - 1; i >= 0; --i) {
const char* function_name =
functions[i]->QualifiedUserVisibleNameCString();
auto* line = address_symbols->add_lines();
line->set_function_name(function_name);
}
} else {
auto* line = address_symbols->add_lines();
line->set_function_name(code.Name());
}
}
if (module_symbols != nullptr) {
WritePacket(&packet);
packet.Reset();
}
}
if (need_to_enter_different_group) {
const bool kBypassSafepoint = false;
Thread::ExitIsolateGroupAsHelper(kBypassSafepoint);
}
}
void TimelineEventPerfettoFileRecorder::NotifyAboutIsolateGroupShutdown(
IsolateGroup* isolate_group) {
SafepointMutexLocker ml(&writer_mutex_);
EmitModuleSymbolsFor(isolate_group);
}
TimelineEventPerfettoFileRecorder::~TimelineEventPerfettoFileRecorder() {
ShutDown();
protozero::HeapBuffered<perfetto::protos::pbzero::TracePacket>& packet =
this->packet();
#if defined(DART_PRECOMPILED_RUNTIME) && defined(DART_INCLUDE_PROFILER)
if (!writer_.interned_data_builder().frames().IsEmpty()) {
Isolate* caller_isolate = Isolate::Current();
IsolateGroup::ForEach([&](auto group) { EmitModuleSymbolsFor(group); });
if (Isolate::Current() != caller_isolate) {
Thread::EnterIsolate(caller_isolate);
}
}
#endif
// We do not need to lock the following section, because at this point
// |RecorderSynchronizationLock| must have been put in a state that prevents
// the metadata maps from being modified.
protozero::HeapBuffered<perfetto::protos::pbzero::TracePacket>& packet =
this->packet();
for (SimpleHashMap::Entry* entry = track_uuid_to_track_metadata().Start();
entry != nullptr; entry = track_uuid_to_track_metadata().Next(entry)) {
TimelineTrackMetadata* value =
@@ -2484,8 +2621,6 @@ void TimelineEventEndlessRecorder::ClearLocked() {
block_index_ = 0;
}
static std::atomic<bool> is_streaming_timeline{false};
bool Timeline::StreamTo(const char* recorder_kind,
const char* file,
const char* streams,
@@ -2512,30 +2647,68 @@ bool Timeline::StreamTo(const char* recorder_kind,
}
Timeline::InitWithRecorder(recorder, streams);
streaming_start_micros.store(OS::GetCurrentMonotonicMicrosForTimeline());
streaming_stop_micros.store(kMaxInt64);
is_streaming_timeline.store(true);
#if defined(DART_INCLUDE_PROFILER) && defined(SUPPORT_PERFETTO)
if (Profiler::IsRunning() && (strcmp(recorder_kind, "perfettofile") == 0)) {
Profiler::SetProfileProcessorCallback([](auto& profile) {
RecorderSynchronizationLockScope ls;
if (recorder_ != nullptr && ls.IsActive() &&
is_streaming_timeline.load()) {
static_cast<TimelineEventPerfettoFileRecorder*>(recorder_)
->WriteProfile(profile);
}
});
}
#endif
return true;
}
void Timeline::StopStreaming() {
is_streaming_timeline.store(false);
#if defined(DART_INCLUDE_PROFILER)
Profiler::SetProfileProcessorCallback(nullptr);
void Timeline::DrainCompletedSampleBlocksIntoRecorder(
NOT_IN_PRECOMPILED(Isolate* isolate)) {
if (!is_streaming_timeline.load()) {
return;
}
#if defined(DART_PRECOMPILED_RUNTIME)
auto& profile = *Profiler::sample_block_buffer();
#else
auto thread = Thread::Current();
if (Isolate::IsSystemIsolate(isolate)) return;
TIMELINE_DURATION(thread, Isolate, "Timeline::WriteProfile")
DisableThreadInterruptsScope dtis(thread);
StackZone zone(thread);
HandleScope handle_scope(thread);
Profile profile;
NoAllocationSampleFilter filter(isolate->main_port(), Thread::kMutatorTask,
streaming_start_micros,
streaming_stop_micros);
profile.Build(thread, isolate, &filter, Profiler::sample_block_buffer());
#endif
RecorderSynchronizationLockScope ls;
if (recorder_ != nullptr && ls.IsActive() && is_streaming_timeline.load()) {
recorder_->WriteProfile(profile);
}
}
#endif
void Timeline::StopStreaming(bool reinitialize) {
if (!is_streaming_timeline.load()) {
return;
}
streaming_stop_micros.store(OS::GetCurrentMonotonicMicrosForTimeline());
is_streaming_timeline.store(false);
Timeline::Cleanup();
Timeline::Init();
if (reinitialize) {
Timeline::Init();
}
}
void Timeline::NotifyAboutIsolateGroupShutdown(IsolateGroup* isolate_group) {
#if defined(DART_INCLUDE_PROFILER) && defined(SUPPORT_PERFETTO)
if (!is_streaming_timeline.load()) {
return;
}
RecorderSynchronizationLockScope ls;
if (recorder_ != nullptr && ls.IsActive()) {
recorder_->NotifyAboutIsolateGroupShutdown(isolate_group);
}
#endif
}
TimelineEventBlock::TimelineEventBlock(intptr_t block_index)
+52 -1
View File
@@ -56,6 +56,7 @@ class JSONWriter;
class Object;
class ObjectPointerVisitor;
class Isolate;
class IsolateGroup;
class Thread;
class TimelineEvent;
class TimelineEventBlock;
@@ -64,6 +65,19 @@ class TimelineStream;
class VirtualMemory;
class Zone;
#if defined(DART_INCLUDE_PROFILER)
// In AOT mode we don't preprocess collected samples into a Profile before
// writing them out. See also Timeline::DrainCompletedSampleBlocksIntoRecorder
// below.
#if defined(DART_PRECOMPILED_RUNTIME)
class SampleBlockBuffer;
using TimelineProfileType = SampleBlockBuffer;
#else
class Profile;
using TimelineProfileType = Profile;
#endif
#endif
#if defined(SUPPORT_TIMELINE)
#define CALLBACK_RECORDER_NAME "Callback"
#define ENDLESS_RECORDER_NAME "Endless"
@@ -238,7 +252,18 @@ class Timeline : public AllStatic {
const char* streams,
const char** error);
static void StopStreaming();
// Stop streaming started by |StreamTo|.
//
// If |reinitialize| is true, restores original recorder configured via
// flags otherwise leaves timeline recording stopped.
static void StopStreaming(bool reinitialize = true);
// Notify current recorder about isolate group shutdown.
//
// Most recorders don't care about this but when streaming Perfetto timeline
// in AOT mode with profiler enabled we might need to emit symbols for
// collected frames into the timeline stream.
static void NotifyAboutIsolateGroupShutdown(IsolateGroup* isolate_group);
// Access the global recorder. Not thread safe.
static TimelineEventRecorder* recorder() { return recorder_; }
@@ -280,6 +305,24 @@ class Timeline : public AllStatic {
TIMELINE_STREAM_LIST(TIMELINE_STREAM_FLAGS)
#undef TIMELINE_STREAM_FLAGS
#if defined(DART_INCLUDE_PROFILER)
// Drains completed sample blocks from Profiler's |SampleBlockBuffer| into
// the timeline recorder. This should only be called when using Perfetto
// recorder (because it is the only one that supports profiling data as
// part of the timeline).
//
// In JIT mode we drain blocks for each isolate independently by processing
// them into |Profile| object. This is an extremely expensive operation
// because it requires stopping the whole isolate group to construct
// code map used for symbolization.
//
// In AOT mode we drain the whole |SampleBlockBuffer| at once and we do not
// symbolize Dart frames as we do it. Instead we expect to emit additional
// symbolization data when recording is complete.
static void DrainCompletedSampleBlocksIntoRecorder(
NOT_IN_PRECOMPILED(Isolate* isolate));
#endif
private:
// Initialize timeline system. Not thread safe.
static void InitWithRecorder(TimelineEventRecorder* recorder,
@@ -930,6 +973,14 @@ class TimelineEventRecorder : public MallocAllocated {
const char* thread_name);
virtual void AddAsyncTrackMetadataBasedOnEvent(const TimelineEvent& event);
virtual void NotifyAboutIsolateGroupShutdown(IsolateGroup* isolate_group) {
// Most recorders don't care about it.
}
#if defined(DART_INCLUDE_PROFILER)
virtual void WriteProfile(TimelineProfileType& profile) {}
#endif
protected:
static constexpr intptr_t kTrackUuidToTrackMetadataInitialCapacity = 1 << 4;
@@ -63,6 +63,11 @@ class InternedData_Decoder
debug_annotation_names() const {
return GetRepeated<::protozero::ConstBytes>(3);
}
bool has_build_ids() const { return at<16>().valid(); }
::protozero::RepeatedFieldIterator<::protozero::ConstBytes> build_ids()
const {
return GetRepeated<::protozero::ConstBytes>(16);
}
bool has_mapping_paths() const { return at<17>().valid(); }
::protozero::RepeatedFieldIterator<::protozero::ConstBytes> mapping_paths()
const {
@@ -100,6 +105,7 @@ class InternedData : public ::protozero::Message {
kEventCategoriesFieldNumber = 1,
kEventNamesFieldNumber = 2,
kDebugAnnotationNamesFieldNumber = 3,
kBuildIdsFieldNumber = 16,
kMappingPathsFieldNumber = 17,
kFunctionNamesFieldNumber = 5,
kMappingsFieldNumber = 19,
@@ -151,6 +157,19 @@ class InternedData : public ::protozero::Message {
return BeginNestedMessage<T>(3);
}
using FieldMetadata_BuildIds = ::protozero::proto_utils::FieldMetadata<
16,
::protozero::proto_utils::RepetitionType::kRepeatedNotPacked,
::protozero::proto_utils::ProtoSchemaType::kMessage,
InternedString,
InternedData>;
static constexpr FieldMetadata_BuildIds kBuildIds{};
template <typename T = InternedString>
T* add_build_ids() {
return BeginNestedMessage<T>(16);
}
using FieldMetadata_MappingPaths = ::protozero::proto_utils::FieldMetadata<
17,
::protozero::proto_utils::RepetitionType::kRepeatedNotPacked,
@@ -65,6 +65,8 @@ message InternedData {
// Note: field IDs up to 15 should be used for frequent data only.
// Build IDs of exectuable files.
repeated InternedString build_ids = 16;
// Paths to executable files.
repeated InternedString mapping_paths = 17;
// Names of functions used in frames below.
@@ -20,6 +20,15 @@
#include "perfetto/protozero/proto_decoder.h"
#include "perfetto/protozero/proto_utils.h"
namespace perfetto {
namespace protos {
namespace pbzero {
class AddressSymbols;
class Line;
} // Namespace pbzero.
} // Namespace protos.
} // Namespace perfetto.
namespace perfetto {
namespace protos {
namespace pbzero {
@@ -206,6 +215,8 @@ class Mapping_Decoder
: TypedProtoDecoder(raw.data, raw.size) {}
bool has_iid() const { return at<1>().valid(); }
uint64_t iid() const { return at<1>().as_uint64(); }
bool has_build_id() const { return at<2>().valid(); }
uint64_t build_id() const { return at<2>().as_uint64(); }
bool has_start_offset() const { return at<3>().valid(); }
uint64_t start_offset() const { return at<3>().as_uint64(); }
bool has_start() const { return at<4>().valid(); }
@@ -223,6 +234,7 @@ class Mapping : public ::protozero::Message {
using Decoder = Mapping_Decoder;
enum : int32_t {
kIidFieldNumber = 1,
kBuildIdFieldNumber = 2,
kStartOffsetFieldNumber = 3,
kStartFieldNumber = 4,
kEndFieldNumber = 5,
@@ -248,6 +260,24 @@ class Mapping : public ::protozero::Message {
value);
}
using FieldMetadata_BuildId = ::protozero::proto_utils::FieldMetadata<
2,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kUint64,
uint64_t,
Mapping>;
static constexpr FieldMetadata_BuildId kBuildId{};
void set_build_id(uint64_t value) {
static constexpr uint32_t field_id = FieldMetadata_BuildId::kFieldId;
// Call the appropriate protozero::Message::Append(field_id, ...)
// method based on the type of the field.
::protozero::internal::FieldWriter<
::protozero::proto_utils::ProtoSchemaType::kUint64>::Append(*this,
field_id,
value);
}
using FieldMetadata_StartOffset = ::protozero::proto_utils::FieldMetadata<
3,
::protozero::proto_utils::RepetitionType::kNotRepeated,
@@ -321,6 +351,258 @@ class Mapping : public ::protozero::Message {
}
};
class ModuleSymbols_Decoder
: public ::protozero::TypedProtoDecoder</*MAX_FIELD_ID=*/3> {
public:
ModuleSymbols_Decoder(const uint8_t* data, size_t len)
: TypedProtoDecoder(data, len) {}
explicit ModuleSymbols_Decoder(const std::string& raw)
: TypedProtoDecoder(reinterpret_cast<const uint8_t*>(raw.data()),
raw.size()) {}
explicit ModuleSymbols_Decoder(const ::protozero::ConstBytes& raw)
: TypedProtoDecoder(raw.data, raw.size) {}
bool has_path() const { return at<1>().valid(); }
::protozero::ConstChars path() const { return at<1>().as_string(); }
bool has_build_id() const { return at<2>().valid(); }
::protozero::ConstChars build_id() const { return at<2>().as_string(); }
bool has_address_symbols() const { return at<3>().valid(); }
::protozero::RepeatedFieldIterator<::protozero::ConstBytes> address_symbols()
const {
return GetRepeated<::protozero::ConstBytes>(3);
}
};
class ModuleSymbols : public ::protozero::Message {
public:
using Decoder = ModuleSymbols_Decoder;
enum : int32_t {
kPathFieldNumber = 1,
kBuildIdFieldNumber = 2,
kAddressSymbolsFieldNumber = 3,
};
static constexpr const char* GetName() {
return ".perfetto.protos.ModuleSymbols";
}
using FieldMetadata_Path = ::protozero::proto_utils::FieldMetadata<
1,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kString,
std::string,
ModuleSymbols>;
static constexpr FieldMetadata_Path kPath{};
void set_path(const char* data, size_t size) {
AppendBytes(FieldMetadata_Path::kFieldId, data, size);
}
void set_path(::protozero::ConstChars chars) {
AppendBytes(FieldMetadata_Path::kFieldId, chars.data, chars.size);
}
void set_path(std::string value) {
static constexpr uint32_t field_id = FieldMetadata_Path::kFieldId;
// Call the appropriate protozero::Message::Append(field_id, ...)
// method based on the type of the field.
::protozero::internal::FieldWriter<
::protozero::proto_utils::ProtoSchemaType::kString>::Append(*this,
field_id,
value);
}
using FieldMetadata_BuildId = ::protozero::proto_utils::FieldMetadata<
2,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kString,
std::string,
ModuleSymbols>;
static constexpr FieldMetadata_BuildId kBuildId{};
void set_build_id(const char* data, size_t size) {
AppendBytes(FieldMetadata_BuildId::kFieldId, data, size);
}
void set_build_id(::protozero::ConstChars chars) {
AppendBytes(FieldMetadata_BuildId::kFieldId, chars.data, chars.size);
}
void set_build_id(std::string value) {
static constexpr uint32_t field_id = FieldMetadata_BuildId::kFieldId;
// Call the appropriate protozero::Message::Append(field_id, ...)
// method based on the type of the field.
::protozero::internal::FieldWriter<
::protozero::proto_utils::ProtoSchemaType::kString>::Append(*this,
field_id,
value);
}
using FieldMetadata_AddressSymbols = ::protozero::proto_utils::FieldMetadata<
3,
::protozero::proto_utils::RepetitionType::kRepeatedNotPacked,
::protozero::proto_utils::ProtoSchemaType::kMessage,
AddressSymbols,
ModuleSymbols>;
static constexpr FieldMetadata_AddressSymbols kAddressSymbols{};
template <typename T = AddressSymbols>
T* add_address_symbols() {
return BeginNestedMessage<T>(3);
}
};
class AddressSymbols_Decoder
: public ::protozero::TypedProtoDecoder</*MAX_FIELD_ID=*/2> {
public:
AddressSymbols_Decoder(const uint8_t* data, size_t len)
: TypedProtoDecoder(data, len) {}
explicit AddressSymbols_Decoder(const std::string& raw)
: TypedProtoDecoder(reinterpret_cast<const uint8_t*>(raw.data()),
raw.size()) {}
explicit AddressSymbols_Decoder(const ::protozero::ConstBytes& raw)
: TypedProtoDecoder(raw.data, raw.size) {}
bool has_address() const { return at<1>().valid(); }
uint64_t address() const { return at<1>().as_uint64(); }
bool has_lines() const { return at<2>().valid(); }
::protozero::RepeatedFieldIterator<::protozero::ConstBytes> lines() const {
return GetRepeated<::protozero::ConstBytes>(2);
}
};
class AddressSymbols : public ::protozero::Message {
public:
using Decoder = AddressSymbols_Decoder;
enum : int32_t {
kAddressFieldNumber = 1,
kLinesFieldNumber = 2,
};
static constexpr const char* GetName() {
return ".perfetto.protos.AddressSymbols";
}
using FieldMetadata_Address = ::protozero::proto_utils::FieldMetadata<
1,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kUint64,
uint64_t,
AddressSymbols>;
static constexpr FieldMetadata_Address kAddress{};
void set_address(uint64_t value) {
static constexpr uint32_t field_id = FieldMetadata_Address::kFieldId;
// Call the appropriate protozero::Message::Append(field_id, ...)
// method based on the type of the field.
::protozero::internal::FieldWriter<
::protozero::proto_utils::ProtoSchemaType::kUint64>::Append(*this,
field_id,
value);
}
using FieldMetadata_Lines = ::protozero::proto_utils::FieldMetadata<
2,
::protozero::proto_utils::RepetitionType::kRepeatedNotPacked,
::protozero::proto_utils::ProtoSchemaType::kMessage,
Line,
AddressSymbols>;
static constexpr FieldMetadata_Lines kLines{};
template <typename T = Line>
T* add_lines() {
return BeginNestedMessage<T>(2);
}
};
class Line_Decoder : public ::protozero::TypedProtoDecoder</*MAX_FIELD_ID=*/3> {
public:
Line_Decoder(const uint8_t* data, size_t len)
: TypedProtoDecoder(data, len) {}
explicit Line_Decoder(const std::string& raw)
: TypedProtoDecoder(reinterpret_cast<const uint8_t*>(raw.data()),
raw.size()) {}
explicit Line_Decoder(const ::protozero::ConstBytes& raw)
: TypedProtoDecoder(raw.data, raw.size) {}
bool has_function_name() const { return at<1>().valid(); }
::protozero::ConstChars function_name() const { return at<1>().as_string(); }
bool has_source_file_name() const { return at<2>().valid(); }
::protozero::ConstChars source_file_name() const {
return at<2>().as_string();
}
bool has_line_number() const { return at<3>().valid(); }
uint32_t line_number() const { return at<3>().as_uint32(); }
};
class Line : public ::protozero::Message {
public:
using Decoder = Line_Decoder;
enum : int32_t {
kFunctionNameFieldNumber = 1,
kSourceFileNameFieldNumber = 2,
kLineNumberFieldNumber = 3,
};
static constexpr const char* GetName() { return ".perfetto.protos.Line"; }
using FieldMetadata_FunctionName = ::protozero::proto_utils::FieldMetadata<
1,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kString,
std::string,
Line>;
static constexpr FieldMetadata_FunctionName kFunctionName{};
void set_function_name(const char* data, size_t size) {
AppendBytes(FieldMetadata_FunctionName::kFieldId, data, size);
}
void set_function_name(::protozero::ConstChars chars) {
AppendBytes(FieldMetadata_FunctionName::kFieldId, chars.data, chars.size);
}
void set_function_name(std::string value) {
static constexpr uint32_t field_id = FieldMetadata_FunctionName::kFieldId;
// Call the appropriate protozero::Message::Append(field_id, ...)
// method based on the type of the field.
::protozero::internal::FieldWriter<
::protozero::proto_utils::ProtoSchemaType::kString>::Append(*this,
field_id,
value);
}
using FieldMetadata_SourceFileName = ::protozero::proto_utils::FieldMetadata<
2,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kString,
std::string,
Line>;
static constexpr FieldMetadata_SourceFileName kSourceFileName{};
void set_source_file_name(const char* data, size_t size) {
AppendBytes(FieldMetadata_SourceFileName::kFieldId, data, size);
}
void set_source_file_name(::protozero::ConstChars chars) {
AppendBytes(FieldMetadata_SourceFileName::kFieldId, chars.data, chars.size);
}
void set_source_file_name(std::string value) {
static constexpr uint32_t field_id = FieldMetadata_SourceFileName::kFieldId;
// Call the appropriate protozero::Message::Append(field_id, ...)
// method based on the type of the field.
::protozero::internal::FieldWriter<
::protozero::proto_utils::ProtoSchemaType::kString>::Append(*this,
field_id,
value);
}
using FieldMetadata_LineNumber = ::protozero::proto_utils::FieldMetadata<
3,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kUint32,
uint32_t,
Line>;
static constexpr FieldMetadata_LineNumber kLineNumber{};
void set_line_number(uint32_t value) {
static constexpr uint32_t field_id = FieldMetadata_LineNumber::kFieldId;
// Call the appropriate protozero::Message::Append(field_id, ...)
// method based on the type of the field.
::protozero::internal::FieldWriter<
::protozero::proto_utils::ProtoSchemaType::kUint32>::Append(*this,
field_id,
value);
}
};
class InternedString_Decoder
: public ::protozero::TypedProtoDecoder</*MAX_FIELD_ID=*/2> {
public:
@@ -37,10 +37,49 @@ message InternedString {
optional bytes str = 2;
}
// Source line info.
message Line {
optional string function_name = 1;
optional string source_file_name = 2;
optional uint32 line_number = 3;
}
// Symbols for a given address in a module.
message AddressSymbols {
optional uint64 address = 1;
// Source lines that correspond to this address.
//
// These are repeated because when inlining happens, multiple functions'
// frames can be at a single address. Imagine function Foo calling the
// std::vector<int> constructor, which gets inlined at 0xf00. We then get
// both Foo and the std::vector<int> constructor when we symbolize the
// address.
repeated Line lines = 2;
}
// Symbols for addresses seen in a module.
// Used in re-symbolisation of complete traces.
message ModuleSymbols {
// Fully qualified path to the mapping.
// E.g. /system/lib64/libc.so.
optional string path = 1;
// .note.gnu.build-id on Linux (not hex encoded).
// uuid on MacOS.
// Module GUID on Windows.
optional string build_id = 2;
repeated AddressSymbols address_symbols = 3;
}
message Mapping {
// Interning key.
optional uint64 iid = 1;
// Interning key.
// Starts from 1, 0 is the same as "not set".
optional uint64 build_id = 2;
// The linker may create multiple memory mappings for the same shared
// library.
// This is so that the ELF header is mapped as read only, while the
@@ -25,6 +25,7 @@ namespace protos {
namespace pbzero {
class ClockSnapshot;
class InternedData;
class ModuleSymbols;
class PerfSample;
class TrackDescriptor;
class TrackEvent;
@@ -91,6 +92,8 @@ class TracePacket_Decoder
::protozero::ConstBytes track_descriptor() const {
return at<60>().as_bytes();
}
bool has_module_symbols() const { return at<61>().valid(); }
::protozero::ConstBytes module_symbols() const { return at<61>().as_bytes(); }
bool has_perf_sample() const { return at<66>().valid(); }
::protozero::ConstBytes perf_sample() const { return at<66>().as_bytes(); }
bool has_trusted_packet_sequence_id() const { return at<10>().valid(); }
@@ -110,6 +113,7 @@ class TracePacket : public ::protozero::Message {
kClockSnapshotFieldNumber = 6,
kTrackEventFieldNumber = 11,
kTrackDescriptorFieldNumber = 60,
kModuleSymbolsFieldNumber = 61,
kPerfSampleFieldNumber = 66,
kTrustedPacketSequenceIdFieldNumber = 10,
kInternedDataFieldNumber = 12,
@@ -207,6 +211,19 @@ class TracePacket : public ::protozero::Message {
return BeginNestedMessage<T>(60);
}
using FieldMetadata_ModuleSymbols = ::protozero::proto_utils::FieldMetadata<
61,
::protozero::proto_utils::RepetitionType::kNotRepeated,
::protozero::proto_utils::ProtoSchemaType::kMessage,
ModuleSymbols,
TracePacket>;
static constexpr FieldMetadata_ModuleSymbols kModuleSymbols{};
template <typename T = ModuleSymbols>
T* set_module_symbols() {
return BeginNestedMessage<T>(61);
}
using FieldMetadata_PerfSample = ::protozero::proto_utils::FieldMetadata<
66,
::protozero::proto_utils::RepetitionType::kNotRepeated,
@@ -26,6 +26,7 @@ syntax = "proto2";
import "protos/perfetto/trace/clock_snapshot.proto";
import "protos/perfetto/trace/interned_data/interned_data.proto";
import "protos/perfetto/trace/profiling/profile_common.proto";
import "protos/perfetto/trace/profiling/profile_packet.proto";
import "protos/perfetto/trace/track_event/track_descriptor.proto";
import "protos/perfetto/trace/track_event/track_event.proto";
@@ -79,6 +80,9 @@ message TracePacket {
// Only used by TrackEvent.
TrackDescriptor track_descriptor = 60;
// Only used in profile packets.
ModuleSymbols module_symbols = 61;
PerfSample perf_sample = 66;
}