[VM] Introduce --profile-startup CLI flag
TEST=pkg/vm_service/test/profile_startup_cli_flag_test Change-Id: I39bf67bf3157fe5700f1a62822f1dc3159c9bc96 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/438980 Reviewed-by: Siva Annamalai <asiva@google.com> Commit-Queue: Derek Xu <derekx@google.com>
This commit is contained in:
@@ -187,7 +187,16 @@ class RunCommand extends DartdevCommand {
|
||||
help: 'Record information about each microtask. Information about '
|
||||
'completed microtasks will be written to the "Microtask" '
|
||||
'timeline stream.',
|
||||
);
|
||||
)
|
||||
..addFlag('profile-startup',
|
||||
hide: !verbose,
|
||||
negatable: false,
|
||||
help: 'Make the profiler discard new samples once the profiler '
|
||||
'sample buffer is full. When this flag is not set, the '
|
||||
'profiler sample buffer is used as a ring buffer, meaning that '
|
||||
'once it is full, new samples start overwriting the oldest '
|
||||
'ones. This flag itself does not enable the profiler; the '
|
||||
'profiler must be enabled separately, e.g. with --profiler.');
|
||||
} else {
|
||||
argParser.addOption('timeline-recorder',
|
||||
help: 'Selects the timeline recorder to use.\n'
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'dart:typed_data';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
import 'package:vm_service_protos/vm_service_protos.dart' show DebugAnnotation;
|
||||
import 'package:vm_service_protos/vm_service_protos.dart' hide Frame;
|
||||
|
||||
typedef IsolateTest = Future<void> Function(
|
||||
VmService service,
|
||||
@@ -955,3 +955,43 @@ Map<String, String> mapFromListOfDebugAnnotations(
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
int computeTimeOriginNanos(List<TracePacket> packets) {
|
||||
final packetsWithPerfSamples =
|
||||
packets.where((packet) => packet.hasPerfSample()).toList();
|
||||
if (packetsWithPerfSamples.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
int smallest = packetsWithPerfSamples.first.timestamp.toInt();
|
||||
for (int i = 0; i < packetsWithPerfSamples.length; i++) {
|
||||
if (packetsWithPerfSamples[i].timestamp < smallest) {
|
||||
smallest = packetsWithPerfSamples[i].timestamp.toInt();
|
||||
}
|
||||
}
|
||||
return smallest;
|
||||
}
|
||||
|
||||
int computeTimeExtentNanos(List<TracePacket> packets, int timeOrigin) {
|
||||
final packetsWithPerfSamples =
|
||||
packets.where((packet) => packet.hasPerfSample()).toList();
|
||||
if (packetsWithPerfSamples.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
int largestExtent = packetsWithPerfSamples[0].timestamp.toInt() - timeOrigin;
|
||||
for (var i = 0; i < packetsWithPerfSamples.length; i++) {
|
||||
final int duration =
|
||||
packetsWithPerfSamples[i].timestamp.toInt() - timeOrigin;
|
||||
if (duration > largestExtent) {
|
||||
largestExtent = duration;
|
||||
}
|
||||
}
|
||||
return largestExtent;
|
||||
}
|
||||
|
||||
Iterable<PerfSample> extractPerfSamplesFromTracePackets(
|
||||
List<TracePacket> packets,
|
||||
) {
|
||||
return packets
|
||||
.where((packet) => packet.hasPerfSample())
|
||||
.map((packet) => packet.perfSample);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import 'dart:convert';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service.dart' hide Timeline;
|
||||
import 'package:vm_service_protos/vm_service_protos.dart';
|
||||
import 'package:vm_service_protos/vm_service_protos.dart'
|
||||
show Trace, TracePacket_SequenceFlags;
|
||||
|
||||
import 'common/service_test_common.dart';
|
||||
import 'common/test_helper.dart';
|
||||
@@ -20,46 +21,6 @@ void testeeDo() {
|
||||
print('Testee did something.');
|
||||
}
|
||||
|
||||
int computeTimeOriginNanos(List<TracePacket> packets) {
|
||||
final packetsWithPerfSamples =
|
||||
packets.where((packet) => packet.hasPerfSample()).toList();
|
||||
if (packetsWithPerfSamples.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
int smallest = packetsWithPerfSamples.first.timestamp.toInt();
|
||||
for (int i = 0; i < packetsWithPerfSamples.length; i++) {
|
||||
if (packetsWithPerfSamples[i].timestamp < smallest) {
|
||||
smallest = packetsWithPerfSamples[i].timestamp.toInt();
|
||||
}
|
||||
}
|
||||
return smallest;
|
||||
}
|
||||
|
||||
int computeTimeExtentNanos(List<TracePacket> packets, int timeOrigin) {
|
||||
final packetsWithPerfSamples =
|
||||
packets.where((packet) => packet.hasPerfSample()).toList();
|
||||
if (packetsWithPerfSamples.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
int largestExtent = packetsWithPerfSamples[0].timestamp.toInt() - timeOrigin;
|
||||
for (var i = 0; i < packetsWithPerfSamples.length; i++) {
|
||||
final int duration =
|
||||
packetsWithPerfSamples[i].timestamp.toInt() - timeOrigin;
|
||||
if (duration > largestExtent) {
|
||||
largestExtent = duration;
|
||||
}
|
||||
}
|
||||
return largestExtent;
|
||||
}
|
||||
|
||||
Iterable<PerfSample> extractPerfSamplesFromTracePackets(
|
||||
List<TracePacket> packets,
|
||||
) {
|
||||
return packets
|
||||
.where((packet) => packet.hasPerfSample())
|
||||
.map((packet) => packet.perfSample);
|
||||
}
|
||||
|
||||
final tests = <IsolateTest>[
|
||||
hasStoppedAtExit,
|
||||
(VmService service, IsolateRef isolateRef) async {
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2025, 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 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service.dart' hide Timeline;
|
||||
import 'package:vm_service_protos/vm_service_protos.dart';
|
||||
|
||||
import 'common/service_test_common.dart';
|
||||
import 'common/test_helper.dart';
|
||||
|
||||
void testeeMain() {
|
||||
print('Testee doing something.');
|
||||
final stopwatch = Stopwatch();
|
||||
stopwatch.start();
|
||||
while (stopwatch.elapsedMilliseconds < 5000) {}
|
||||
stopwatch.stop();
|
||||
print('Testee did something.');
|
||||
}
|
||||
|
||||
final tests = <IsolateTest>[
|
||||
hasStoppedAtExit,
|
||||
(VmService service, IsolateRef isolateRef) async {
|
||||
// The purpose of this test is to ensure that when the `--profile-startup`
|
||||
// CLI flag is set, the profiler discards any samples collected after when
|
||||
// the sample buffer has filled up. This test does not check the contents of
|
||||
// the samples returned by [service.getPerfettoCpuSamples],
|
||||
// `get_perfetto_cpu_samples_rpc_test` does.
|
||||
//
|
||||
// `--max-profile-depth=2` and `--sample-buffer-duration=1` are passed to
|
||||
// [runIsolateTests] below, and [testeeMain] spins for 5 seconds, so the
|
||||
// profiler sample buffer should be full once [testeeMain] has finished
|
||||
// running. If `--profile-startup` is working as intended, then the two
|
||||
// [service.getPerfettoCpuSamples] calls below should deliver consistent
|
||||
// results, because no samples will ever be overwritten by newer ones.
|
||||
|
||||
final result = await service.getPerfettoCpuSamples(isolateRef.id!);
|
||||
final trace = Trace.fromBuffer(base64Decode(result.samples!));
|
||||
final packets = trace.packet;
|
||||
final perfSamples = extractPerfSamplesFromTracePackets(packets);
|
||||
expect(perfSamples.length, isPositive);
|
||||
|
||||
// Calculate the time window of events.
|
||||
final timeOriginNanos = computeTimeOriginNanos(packets);
|
||||
final timeExtentNanos = computeTimeExtentNanos(packets, timeOriginNanos);
|
||||
print(
|
||||
'Requesting CPU samples within the filter window of '
|
||||
'timeOriginNanos=$timeOriginNanos and timeExtentNanos=$timeExtentNanos',
|
||||
);
|
||||
// Query for the samples within the time window.
|
||||
final filteredResult = await service.getPerfettoCpuSamples(
|
||||
isolateRef.id!,
|
||||
timeOriginMicros: timeOriginNanos ~/ 1000,
|
||||
timeExtentMicros: timeExtentNanos ~/ 1000,
|
||||
);
|
||||
final filteredTrace =
|
||||
Trace.fromBuffer(base64Decode(filteredResult.samples!));
|
||||
final filteredPackets = filteredTrace.packet;
|
||||
final filteredTraceTimeOriginNanos =
|
||||
computeTimeOriginNanos(filteredPackets);
|
||||
final filteredTraceTimeExtentNanos = computeTimeExtentNanos(
|
||||
filteredPackets,
|
||||
filteredTraceTimeOriginNanos,
|
||||
);
|
||||
print(
|
||||
'The returned CPU samples span a time window of '
|
||||
'timeOriginNanos=$filteredTraceTimeOriginNanos and '
|
||||
'timeExtentNanos=$filteredTraceTimeExtentNanos',
|
||||
);
|
||||
// Verify that [result] and [filteredResult] have the same number of
|
||||
// [PerfSample]s.
|
||||
expect(
|
||||
extractPerfSamplesFromTracePackets(filteredPackets).length,
|
||||
perfSamples.length,
|
||||
);
|
||||
|
||||
// The profiler gets another chance to collect samples when handling each
|
||||
// `getPerfettoCpuSamples` request, so we can verify that the sample buffer
|
||||
// was indeed full when the first `getPerfettoCpuSamples` request was made
|
||||
// by making another unfiltered request, and checking that the number of
|
||||
// samples in the response is the same as the number in the first response.
|
||||
final secondUnfilteredResult =
|
||||
await service.getPerfettoCpuSamples(isolateRef.id!);
|
||||
final secondUnfilteredTrace =
|
||||
Trace.fromBuffer(base64Decode(secondUnfilteredResult.samples!));
|
||||
expect(
|
||||
extractPerfSamplesFromTracePackets(secondUnfilteredTrace.packet).length,
|
||||
perfSamples.length,
|
||||
);
|
||||
},
|
||||
];
|
||||
|
||||
Future<void> main([args = const <String>[]]) => runIsolateTests(
|
||||
args,
|
||||
tests,
|
||||
'profile_startup_cli_flag_test.dart',
|
||||
testeeBefore: testeeMain,
|
||||
pauseOnExit: true,
|
||||
extraArgs: [
|
||||
'--profiler',
|
||||
'--profile-startup',
|
||||
'--profile-period=100',
|
||||
'--max-profile-depth=2',
|
||||
'--sample-buffer-duration=1',
|
||||
],
|
||||
);
|
||||
@@ -216,6 +216,12 @@ void Options::PrintUsage() {
|
||||
"--profile-microtasks\n"
|
||||
" Record information about each microtask. Information about completed\n"
|
||||
" microtasks will be written to the \"Microtask\" timeline stream.\n"
|
||||
"--profile-startup\n"
|
||||
" Make the profiler discard new samples once the profiler sample buffer is\n"
|
||||
" full. When this flag is not set, the profiler sample buffer is used as a\n"
|
||||
" ring buffer, meaning that once it is full, new samples start overwriting\n"
|
||||
" the oldest ones. This flag itself does not enable the profiler; the\n"
|
||||
" profiler must be enabled separately, e.g. with --profiler.\n"
|
||||
#endif // !defined(PRODUCT)
|
||||
"--version\n"
|
||||
" Print the VM version.\n"
|
||||
@@ -516,6 +522,7 @@ bool Options::ProcessVMDebuggingOptions(const char* arg,
|
||||
V("--no-warn-on-pause-with-no-debugger", arg) \
|
||||
V("--timeline-streams", arg) \
|
||||
V("--timeline-recorder", arg) \
|
||||
V("--profile-startup", arg) \
|
||||
V("--enable-experiment", arg)
|
||||
HANDLE_DARTDEV_VM_DEBUG_OPTIONS(IS_DEBUG_OPTION, arg);
|
||||
|
||||
|
||||
+26
-10
@@ -63,6 +63,15 @@ DEFINE_FLAG(
|
||||
"N seconds of samples at a given sample rate. If not provided, the "
|
||||
"default is ~4 seconds. Large values will greatly increase memory "
|
||||
"consumption.");
|
||||
DEFINE_FLAG(
|
||||
bool,
|
||||
profile_startup,
|
||||
false,
|
||||
"Make the profiler discard new samples once the profiler sample buffer is "
|
||||
"full. When this flag is not set, the profiler sample buffer is used as a "
|
||||
"ring buffer, meaning that once it is full, new samples start overwriting "
|
||||
"the oldest ones. This flag itself does not enable the profiler; the "
|
||||
"profiler must be enabled separately, e.g. with --profiler.");
|
||||
|
||||
// Include native stack dumping helpers into AOT compiler even in PRODUCT
|
||||
// mode. This allows to report more informative errors when gen_snapshot
|
||||
@@ -740,17 +749,24 @@ SampleBlock* SampleBlockBuffer::ReserveSampleBlock() {
|
||||
i = (i + 1) % capacity;
|
||||
} while (i != start);
|
||||
|
||||
// No free blocks: try for completed block instead.
|
||||
i = start;
|
||||
do {
|
||||
SampleBlock* block = &blocks_[i];
|
||||
if (block->TryAllocateCompleted()) {
|
||||
return block;
|
||||
}
|
||||
i = (i + 1) % capacity;
|
||||
} while (i != start);
|
||||
if (FLAG_profile_startup) {
|
||||
// There are no free blocks and [FLAG_profile_startup] is set, so we stop
|
||||
// recording samples.
|
||||
return nullptr;
|
||||
} else {
|
||||
// There are no free blocks and [FLAG_profile_startup] is not set, so we
|
||||
// reuse a completed block if one is available.
|
||||
i = start;
|
||||
do {
|
||||
SampleBlock* block = &blocks_[i];
|
||||
if (block->TryAllocateCompleted()) {
|
||||
return block;
|
||||
}
|
||||
i = (i + 1) % capacity;
|
||||
} while (i != start);
|
||||
|
||||
return nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void SampleBlockBuffer::FreeCompletedBlocks() {
|
||||
|
||||
Reference in New Issue
Block a user