[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:
Derek Xu
2025-07-24 12:04:17 -07:00
committed by Commit Queue
parent 2254755d65
commit 9b89266f43
6 changed files with 194 additions and 53 deletions
+10 -1
View File
@@ -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',
],
);
+7
View File
@@ -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
View File
@@ -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() {