diff --git a/pkg/perf_witness/pubspec.yaml b/pkg/perf_witness/pubspec.yaml index c0ee5c485d3..9907721b110 100644 --- a/pkg/perf_witness/pubspec.yaml +++ b/pkg/perf_witness/pubspec.yaml @@ -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 diff --git a/pkg/perf_witness/test/common/busy_loop.dart b/pkg/perf_witness/test/common/busy_loop.dart index e114030fdcf..b898b814756 100644 --- a/pkg/perf_witness/test/common/busy_loop.dart +++ b/pkg/perf_witness/test/common/busy_loop.dart @@ -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 busyLoop({required String name}) async { +Future 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 = []; + 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 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 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 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); } diff --git a/pkg/perf_witness/test/common/simple_hot_loop.dart b/pkg/perf_witness/test/common/simple_hot_loop.dart new file mode 100644 index 00000000000..4deb9024360 --- /dev/null +++ b/pkg/perf_witness/test/common/simple_hot_loop.dart @@ -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 = []; + for (var i = 0; i < 10000; i++) { + l.add(i * i); + } + sum += l[50]; + } + return sum; +} + +void main() { + print(hotLoop()); +} diff --git a/pkg/perf_witness/test/common/test_utils.dart b/pkg/perf_witness/test/common/test_utils.dart new file mode 100644 index 00000000000..364d7f46afa --- /dev/null +++ b/pkg/perf_witness/test/common/test_utils.dart @@ -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 = {}; + final seenStacks = >{}; + final seenTracks = {}; + final seenTrackDescriptors = {}; + + 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> get flattenedSeenStacks { + return seenStacks.map( + (stack) => stack + .expand((f) => f.functionNames ?? const ['']) + .toList(), + ); + } + + bool hasSeenStack(List expectedStack) { + return seenStacks.firstWhereOrNull( + (stack) => stackMatches(stack, expectedStack), + ) != + null; + } +} + +bool stackMatches(List stack, List 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? 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 = {}; + final functionNames = {}; + final frames = {}; + final stacks = >{}; + final mappingPaths = {}; + final mappings = {}; + final buildIds = {}; + + 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; + } + } + } + } +} diff --git a/pkg/perf_witness/test/recorder_server_test.dart b/pkg/perf_witness/test/recorder_server_test.dart index 0de971bca24..f676bb9fd90 100644 --- a/pkg/perf_witness/test/recorder_server_test.dart +++ b/pkg/perf_witness/test/recorder_server_test.dart @@ -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? 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 = []; 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 = []; 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() - .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.', + ]), + 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 = {}; - - void update(InternedData internedData) { - for (var eventName in internedData.eventNames) { - eventNames[eventName.iid.toInt()] = eventName.name; - } - } -} - -Set extractSeenEvents(Trace trace) { - var state = IncrementalState(); - final seenEvents = {}; - final seenTracks = {}; - final seenTrackDescriptors = {}; - 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() + .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.', + ]), + 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() + .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.', + ]), + 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() + .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.', + ]), + 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() + .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.', + // 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); + }); + }); } diff --git a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pb.dart b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pb.dart index d9a0ee3e281..64d16cd17dc 100644 --- a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pb.dart +++ b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pb.dart @@ -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 = diff --git a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pbjson.dart b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pbjson.dart index 7d6bd139f91..0eeb6dc67ac 100644 --- a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pbjson.dart +++ b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/interned_data/interned_data.pbjson.dart @@ -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='); diff --git a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pb.dart b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pb.dart index 3ca38e17dc6..53e14890362 100644 --- a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pb.dart +++ b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pb.dart @@ -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(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? 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(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(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` constructor, which gets inlined at 0xf00. We then get + /// both Foo and the `std::vector` constructor when we symbolize the + /// address. + @$pb.TagNumber(2) + $pb.PbList 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, + }) { + 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(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(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 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 { diff --git a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pbjson.dart b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pbjson.dart index d9d1d0e28ba..b053a0f09a2 100644 --- a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pbjson.dart +++ b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/profiling/profile_common.pbjson.dart @@ -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 = { diff --git a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pb.dart b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pb.dart index 07d496d553c..334c432ec4c 100644 --- a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pb.dart +++ b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pb.dart @@ -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 = diff --git a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pbjson.dart b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pbjson.dart index d1b639d1ac9..68fc3f97506 100644 --- a/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pbjson.dart +++ b/pkg/vm_service_protos/lib/src/protos/perfetto/trace/trace_packet.pbjson.dart @@ -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'); diff --git a/runtime/lib/developer.cc b/runtime/lib/developer.cc index 897dc1d12e9..931f8094a4a 100644 --- a/runtime/lib/developer.cc +++ b/runtime/lib/developer.cc @@ -248,6 +248,11 @@ DEFINE_NATIVE_ENTRY(Developer_NativeRuntime_streamTimelineTo, 0, 5) { Profiler::SetConfig({ .enabled = true, .period_us = static_cast(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 diff --git a/runtime/tests/vm/dart/stream_timeline_to_test.dart b/runtime/tests/vm/dart/stream_timeline_to_test.dart index ec8896cd458..4ec032d4725 100644 --- a/runtime/tests/vm/dart/stream_timeline_to_test.dart +++ b/runtime/tests/vm/dart/stream_timeline_to_test.dart @@ -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 testPerfettoRecorder({ '$perfettoTimeline does not exist', ); - final trace = Trace()..mergeFromBuffer(perfettoTimeline.readAsBytesSync()); - Expect.isNotEmpty(trace.packet); - - var state = IncrementalState(); - final seenEvents = {}; - final seenStacks = >{}; - final seenTracks = {}; - final seenTrackDescriptors = {}; - 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 stack, List 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 = {}; - final functionNames = {}; - final frames = {}; - final stacks = >{}; - - 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); - } - } -} diff --git a/runtime/vm/dart.cc b/runtime/vm/dart.cc index b7620d3a095..b0b8af62b41 100644 --- a/runtime/vm/dart.cc +++ b/runtime/vm/dart.cc @@ -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. diff --git a/runtime/vm/hash_map.h b/runtime/vm/hash_map.h index 51ee4e85429..72eaa522d91 100644 --- a/runtime/vm/hash_map.h +++ b/runtime/vm/hash_map.h @@ -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 +concept DefinesHashAndEquality = requires(const T& a, const T& b) { + { a.Equals(b) } -> std::same_as; + { a.Hash() } -> std::same_as; +}; + +template 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 +template using PointerSet = DirectChainedHashMap>; template diff --git a/runtime/vm/hash_map_test.cc b/runtime/vm/hash_map_test.cc index cc57f268d59..05aa2f23227 100644 --- a/runtime/vm/hash_map_test.cc +++ b/runtime/vm/hash_map_test.cc @@ -14,7 +14,7 @@ class TestValue { // FinalizeHash is used here to provide coverage for FinalizeHash(...) // function. uword Hash() const { return FinalizeHash(static_cast(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_; diff --git a/runtime/vm/heap/pages.h b/runtime/vm/heap/pages.h index c5c23bf9549..970a4c9c2fe 100644 --- a/runtime/vm/heap/pages.h +++ b/runtime/vm/heap/pages.h @@ -224,6 +224,14 @@ class PageSpace { return size >> kWordSizeLog2; } + template + 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; diff --git a/runtime/vm/isolate.cc b/runtime/vm/isolate.cc index 64e652b25d3..e58394c2dbe 100644 --- a/runtime/vm/isolate.cc +++ b/runtime/vm/isolate.cc @@ -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, diff --git a/runtime/vm/perfetto_utils.h b/runtime/vm/perfetto_utils.h index ae9b8e43345..c78c450f14c 100644 --- a/runtime/vm/perfetto_utils.h +++ b/runtime/vm/perfetto_utils.h @@ -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 -struct InternedBytes { - InternedBytes(const T* data, intptr_t length) - : data(data), - length(length), - hash(HashBytes(reinterpret_cast(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 + Span Copy(Allocator* allocator) const { + T* copy = allocator->template Alloc(length); + memcpy(copy, data, length * sizeof(T)); // NOLINT + return {copy, length}; + } - bool Equals(const InternedBytes& other) const { + template + void Dispose(Allocator* allocator) const { + if constexpr (Allocator::kSupportsFreeingIndividualAllocations) { + allocator->Free(const_cast(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(data), + length * sizeof(T)); + } +}; + +template +concept DefinesCopyAndDispose = requires(const T& a, Allocator* allocator) { + { a.Copy(allocator) } -> std::same_as; + { a.Dispose(allocator) } -> std::same_as; +}; + +// Sequence of elements which can be interned by |BytesInterner|. +// +// Equality and hash are defined in terms of raw byte content. +template +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) { + return data.Equals(other.data); + } else { + return memcmp(&data, &other.data, sizeof(T)) == 0; + } + } + + static uword ComputeHash(const T& data) { + if constexpr (DefinesHashAndEquality) { + return data.Hash(); + } else { + return HashBytes(reinterpret_cast(&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 -class BytesInterner - : public BaseDirectChainedHashMap>, +class Interner + : public BaseDirectChainedHashMap>, ValueObject, Allocator> { - using Base = - BaseDirectChainedHashMap>, - ValueObject, - Allocator>; + using Base = BaseDirectChainedHashMap>, + 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 key(data); + if (auto interned = Base::Lookup(&key)) { + return (*interned)->iid; + } + return 0; + } + + uint64_t Intern(const T& data) { state_ |= kInternerWasUsed; - InternedBytes key(data, length); + Interned 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** begin() { return &Base::pairs_[0]; } + Interned** end() { return &Base::pairs_[Base::next_pair_index_]; } + + const Interned** begin() const { return &Base::pairs_[0]; } + const Interned** 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* Copy(const InternedBytes& interned, uint64_t iid) const { - auto data_copy = allocator()->template Alloc(interned.length); - memcpy(data_copy, interned.data, interned.length * sizeof(T)); // NOLINT - auto copy = allocator()->template Alloc>(1); - new (copy) InternedBytes(data_copy, interned.length, interned.hash, iid); + Interned* Copy(const Interned& interned, uint64_t iid) const { + auto copy = allocator()->template Alloc>(1); + if constexpr (DefinesCopyAndDispose) { + new (copy) + Interned(interned.data.Copy(allocator()), interned.hash, iid); + } else { + new (copy) Interned(interned.data, interned.hash, iid); + } return copy; } - void Dispose(InternedBytes* interned) { + void Dispose(Interned* interned) { if constexpr (Allocator::kSupportsFreeingIndividualAllocations) { - allocator()->Free(const_cast(interned->data), - interned->length * sizeof(T)); + if constexpr (DefinesCopyAndDispose) { + 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(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(strlen(str) + 1)}); } InternerStateBits TakeAndResetState() { @@ -270,13 +344,23 @@ class StringInterner : public ValueObject { template 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>** begin() { return bytes_interner_.begin(); } + Interned>** end() { return bytes_interner_.end(); } + + const Interned>** begin() const { return bytes_interner_.begin(); } + const Interned>** end() const { return bytes_interner_.end(); } + private: - BytesInterner bytes_interner_; + Interner, 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) \ + 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& name() { \ - return name##_; \ - } - PERFETTO_INTERNED_RAW_BYTES_FIELDS_LIST(DEFINE_GETTER) + perfetto_utils::Interner& 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 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 name##_; - PERFETTO_INTERNED_RAW_BYTES_FIELDS_LIST(DEFINE_FIELD) + perfetto_utils::Interner name##_; + PERFETTO_INTERNED_FIELDS_LIST(DEFINE_FIELD) #undef DEFINE_FIELD DISALLOW_COPY_AND_ASSIGN(InternedDataBuilder); diff --git a/runtime/vm/profiler.cc b/runtime/vm/profiler.cc index abcbbc20e63..72759b46a20 100644 --- a/runtime/vm/profiler.cc +++ b/runtime/vm/profiler.cc @@ -4,11 +4,16 @@ #include "vm/profiler.h" +#include + #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 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(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(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 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(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(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 mappings_; + char name_buf_[1024]; + + perfetto_utils::InternedDataBuilder& interned_data_builder_; + + bool clock_snapshot_written_ = false; + protozero::HeapBuffered packet_; + MallocGrowableArray 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) diff --git a/runtime/vm/profiler.h b/runtime/vm/profiler.h index 9513e4db778..c5624f10a87 100644 --- a/runtime/vm/profiler.h +++ b/runtime/vm/profiler.h @@ -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 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: diff --git a/runtime/vm/profiler_service.cc b/runtime/vm/profiler_service.cc index 40a1bc8f6e2..8a5e9ca079d 100644 --- a/runtime/vm/profiler_service.cc +++ b/runtime/vm/profiler_service.cc @@ -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(mapping_iid), + .function_name_iid = static_cast(function_name_iid)})); thread->CheckForSafepoint(); } diff --git a/runtime/vm/timeline.cc b/runtime/vm/timeline.cc index 2f87877b67b..0cabeb24487 100644 --- a/runtime/vm/timeline.cc +++ b/runtime/vm/timeline.cc @@ -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* Timeline::enabled_streams_ = nullptr; bool Timeline::recorder_discards_clock_values_ = false; +static std::atomic is_streaming_timeline{false}; +static RelaxedAtomic streaming_start_micros = 0; +static RelaxedAtomic 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* 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(stream)->Write( + static_cast(buffer), length); + }); +#else profile.PrintProfilePerfetto( writer_.interned_data_builder(), this, [](auto buffer, auto length, auto stream) { static_cast(stream)->Write( static_cast(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 functions; + GrowableArray 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& 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& 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& 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 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(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) diff --git a/runtime/vm/timeline.h b/runtime/vm/timeline.h index 9610e8f1c0b..40e135bfcbb 100644 --- a/runtime/vm/timeline.h +++ b/runtime/vm/timeline.h @@ -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; diff --git a/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.pbzero.h b/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.pbzero.h index 7e085f26c14..2f7cf9e18d0 100644 --- a/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.pbzero.h +++ b/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.pbzero.h @@ -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(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 + T* add_build_ids() { + return BeginNestedMessage(16); + } + using FieldMetadata_MappingPaths = ::protozero::proto_utils::FieldMetadata< 17, ::protozero::proto_utils::RepetitionType::kRepeatedNotPacked, diff --git a/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.proto b/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.proto index be5731b6a42..04e903e284b 100644 --- a/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.proto +++ b/third_party/perfetto/protos/perfetto/trace/interned_data/interned_data.proto @@ -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. diff --git a/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.pbzero.h b/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.pbzero.h index a4c2a90ffae..fa71f6cf3f0 100644 --- a/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.pbzero.h +++ b/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.pbzero.h @@ -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 { + public: + ModuleSymbols_Decoder(const uint8_t* data, size_t len) + : TypedProtoDecoder(data, len) {} + explicit ModuleSymbols_Decoder(const std::string& raw) + : TypedProtoDecoder(reinterpret_cast(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 + T* add_address_symbols() { + return BeginNestedMessage(3); + } +}; + +class AddressSymbols_Decoder + : public ::protozero::TypedProtoDecoder { + public: + AddressSymbols_Decoder(const uint8_t* data, size_t len) + : TypedProtoDecoder(data, len) {} + explicit AddressSymbols_Decoder(const std::string& raw) + : TypedProtoDecoder(reinterpret_cast(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 + T* add_lines() { + return BeginNestedMessage(2); + } +}; + +class Line_Decoder : public ::protozero::TypedProtoDecoder { + public: + Line_Decoder(const uint8_t* data, size_t len) + : TypedProtoDecoder(data, len) {} + explicit Line_Decoder(const std::string& raw) + : TypedProtoDecoder(reinterpret_cast(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 { public: diff --git a/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.proto b/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.proto index 5880491d403..e568cd75c26 100644 --- a/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.proto +++ b/third_party/perfetto/protos/perfetto/trace/profiling/profile_common.proto @@ -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 constructor, which gets inlined at 0xf00. We then get + // both Foo and the std::vector 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 diff --git a/third_party/perfetto/protos/perfetto/trace/trace_packet.pbzero.h b/third_party/perfetto/protos/perfetto/trace/trace_packet.pbzero.h index 97d55744be2..d7b245678ec 100644 --- a/third_party/perfetto/protos/perfetto/trace/trace_packet.pbzero.h +++ b/third_party/perfetto/protos/perfetto/trace/trace_packet.pbzero.h @@ -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(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 + T* set_module_symbols() { + return BeginNestedMessage(61); + } + using FieldMetadata_PerfSample = ::protozero::proto_utils::FieldMetadata< 66, ::protozero::proto_utils::RepetitionType::kNotRepeated, diff --git a/third_party/perfetto/protos/perfetto/trace/trace_packet.proto b/third_party/perfetto/protos/perfetto/trace/trace_packet.proto index 6b30a882f69..4977884b565 100644 --- a/third_party/perfetto/protos/perfetto/trace/trace_packet.proto +++ b/third_party/perfetto/protos/perfetto/trace/trace_packet.proto @@ -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; }