Skip to content

Commit 9b89266

Browse files
derekxu16Commit Queue
authored andcommitted
[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 <[email protected]> Commit-Queue: Derek Xu <[email protected]>
1 parent 2254755 commit 9b89266

File tree

6 files changed

+194
-53
lines changed

6 files changed

+194
-53
lines changed

pkg/dartdev/lib/src/commands/run.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,16 @@ class RunCommand extends DartdevCommand {
187187
help: 'Record information about each microtask. Information about '
188188
'completed microtasks will be written to the "Microtask" '
189189
'timeline stream.',
190-
);
190+
)
191+
..addFlag('profile-startup',
192+
hide: !verbose,
193+
negatable: false,
194+
help: 'Make the profiler discard new samples once the profiler '
195+
'sample buffer is full. When this flag is not set, the '
196+
'profiler sample buffer is used as a ring buffer, meaning that '
197+
'once it is full, new samples start overwriting the oldest '
198+
'ones. This flag itself does not enable the profiler; the '
199+
'profiler must be enabled separately, e.g. with --profiler.');
191200
} else {
192201
argParser.addOption('timeline-recorder',
193202
help: 'Selects the timeline recorder to use.\n'

pkg/vm_service/test/common/service_test_common.dart

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import 'dart:typed_data';
1111
import 'package:path/path.dart' as p;
1212
import 'package:test/test.dart';
1313
import 'package:vm_service/vm_service.dart';
14-
import 'package:vm_service_protos/vm_service_protos.dart' show DebugAnnotation;
14+
import 'package:vm_service_protos/vm_service_protos.dart' hide Frame;
1515

1616
typedef IsolateTest = Future<void> Function(
1717
VmService service,
@@ -955,3 +955,43 @@ Map<String, String> mapFromListOfDebugAnnotations(
955955
}),
956956
);
957957
}
958+
959+
int computeTimeOriginNanos(List<TracePacket> packets) {
960+
final packetsWithPerfSamples =
961+
packets.where((packet) => packet.hasPerfSample()).toList();
962+
if (packetsWithPerfSamples.isEmpty) {
963+
return 0;
964+
}
965+
int smallest = packetsWithPerfSamples.first.timestamp.toInt();
966+
for (int i = 0; i < packetsWithPerfSamples.length; i++) {
967+
if (packetsWithPerfSamples[i].timestamp < smallest) {
968+
smallest = packetsWithPerfSamples[i].timestamp.toInt();
969+
}
970+
}
971+
return smallest;
972+
}
973+
974+
int computeTimeExtentNanos(List<TracePacket> packets, int timeOrigin) {
975+
final packetsWithPerfSamples =
976+
packets.where((packet) => packet.hasPerfSample()).toList();
977+
if (packetsWithPerfSamples.isEmpty) {
978+
return 0;
979+
}
980+
int largestExtent = packetsWithPerfSamples[0].timestamp.toInt() - timeOrigin;
981+
for (var i = 0; i < packetsWithPerfSamples.length; i++) {
982+
final int duration =
983+
packetsWithPerfSamples[i].timestamp.toInt() - timeOrigin;
984+
if (duration > largestExtent) {
985+
largestExtent = duration;
986+
}
987+
}
988+
return largestExtent;
989+
}
990+
991+
Iterable<PerfSample> extractPerfSamplesFromTracePackets(
992+
List<TracePacket> packets,
993+
) {
994+
return packets
995+
.where((packet) => packet.hasPerfSample())
996+
.map((packet) => packet.perfSample);
997+
}

pkg/vm_service/test/get_perfetto_cpu_samples_rpc_test.dart

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import 'dart:convert';
66

77
import 'package:test/test.dart';
88
import 'package:vm_service/vm_service.dart' hide Timeline;
9-
import 'package:vm_service_protos/vm_service_protos.dart';
9+
import 'package:vm_service_protos/vm_service_protos.dart'
10+
show Trace, TracePacket_SequenceFlags;
1011

1112
import 'common/service_test_common.dart';
1213
import 'common/test_helper.dart';
@@ -20,46 +21,6 @@ void testeeDo() {
2021
print('Testee did something.');
2122
}
2223

23-
int computeTimeOriginNanos(List<TracePacket> packets) {
24-
final packetsWithPerfSamples =
25-
packets.where((packet) => packet.hasPerfSample()).toList();
26-
if (packetsWithPerfSamples.isEmpty) {
27-
return 0;
28-
}
29-
int smallest = packetsWithPerfSamples.first.timestamp.toInt();
30-
for (int i = 0; i < packetsWithPerfSamples.length; i++) {
31-
if (packetsWithPerfSamples[i].timestamp < smallest) {
32-
smallest = packetsWithPerfSamples[i].timestamp.toInt();
33-
}
34-
}
35-
return smallest;
36-
}
37-
38-
int computeTimeExtentNanos(List<TracePacket> packets, int timeOrigin) {
39-
final packetsWithPerfSamples =
40-
packets.where((packet) => packet.hasPerfSample()).toList();
41-
if (packetsWithPerfSamples.isEmpty) {
42-
return 0;
43-
}
44-
int largestExtent = packetsWithPerfSamples[0].timestamp.toInt() - timeOrigin;
45-
for (var i = 0; i < packetsWithPerfSamples.length; i++) {
46-
final int duration =
47-
packetsWithPerfSamples[i].timestamp.toInt() - timeOrigin;
48-
if (duration > largestExtent) {
49-
largestExtent = duration;
50-
}
51-
}
52-
return largestExtent;
53-
}
54-
55-
Iterable<PerfSample> extractPerfSamplesFromTracePackets(
56-
List<TracePacket> packets,
57-
) {
58-
return packets
59-
.where((packet) => packet.hasPerfSample())
60-
.map((packet) => packet.perfSample);
61-
}
62-
6324
final tests = <IsolateTest>[
6425
hasStoppedAtExit,
6526
(VmService service, IsolateRef isolateRef) async {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:test/test.dart';
8+
import 'package:vm_service/vm_service.dart' hide Timeline;
9+
import 'package:vm_service_protos/vm_service_protos.dart';
10+
11+
import 'common/service_test_common.dart';
12+
import 'common/test_helper.dart';
13+
14+
void testeeMain() {
15+
print('Testee doing something.');
16+
final stopwatch = Stopwatch();
17+
stopwatch.start();
18+
while (stopwatch.elapsedMilliseconds < 5000) {}
19+
stopwatch.stop();
20+
print('Testee did something.');
21+
}
22+
23+
final tests = <IsolateTest>[
24+
hasStoppedAtExit,
25+
(VmService service, IsolateRef isolateRef) async {
26+
// The purpose of this test is to ensure that when the `--profile-startup`
27+
// CLI flag is set, the profiler discards any samples collected after when
28+
// the sample buffer has filled up. This test does not check the contents of
29+
// the samples returned by [service.getPerfettoCpuSamples],
30+
// `get_perfetto_cpu_samples_rpc_test` does.
31+
//
32+
// `--max-profile-depth=2` and `--sample-buffer-duration=1` are passed to
33+
// [runIsolateTests] below, and [testeeMain] spins for 5 seconds, so the
34+
// profiler sample buffer should be full once [testeeMain] has finished
35+
// running. If `--profile-startup` is working as intended, then the two
36+
// [service.getPerfettoCpuSamples] calls below should deliver consistent
37+
// results, because no samples will ever be overwritten by newer ones.
38+
39+
final result = await service.getPerfettoCpuSamples(isolateRef.id!);
40+
final trace = Trace.fromBuffer(base64Decode(result.samples!));
41+
final packets = trace.packet;
42+
final perfSamples = extractPerfSamplesFromTracePackets(packets);
43+
expect(perfSamples.length, isPositive);
44+
45+
// Calculate the time window of events.
46+
final timeOriginNanos = computeTimeOriginNanos(packets);
47+
final timeExtentNanos = computeTimeExtentNanos(packets, timeOriginNanos);
48+
print(
49+
'Requesting CPU samples within the filter window of '
50+
'timeOriginNanos=$timeOriginNanos and timeExtentNanos=$timeExtentNanos',
51+
);
52+
// Query for the samples within the time window.
53+
final filteredResult = await service.getPerfettoCpuSamples(
54+
isolateRef.id!,
55+
timeOriginMicros: timeOriginNanos ~/ 1000,
56+
timeExtentMicros: timeExtentNanos ~/ 1000,
57+
);
58+
final filteredTrace =
59+
Trace.fromBuffer(base64Decode(filteredResult.samples!));
60+
final filteredPackets = filteredTrace.packet;
61+
final filteredTraceTimeOriginNanos =
62+
computeTimeOriginNanos(filteredPackets);
63+
final filteredTraceTimeExtentNanos = computeTimeExtentNanos(
64+
filteredPackets,
65+
filteredTraceTimeOriginNanos,
66+
);
67+
print(
68+
'The returned CPU samples span a time window of '
69+
'timeOriginNanos=$filteredTraceTimeOriginNanos and '
70+
'timeExtentNanos=$filteredTraceTimeExtentNanos',
71+
);
72+
// Verify that [result] and [filteredResult] have the same number of
73+
// [PerfSample]s.
74+
expect(
75+
extractPerfSamplesFromTracePackets(filteredPackets).length,
76+
perfSamples.length,
77+
);
78+
79+
// The profiler gets another chance to collect samples when handling each
80+
// `getPerfettoCpuSamples` request, so we can verify that the sample buffer
81+
// was indeed full when the first `getPerfettoCpuSamples` request was made
82+
// by making another unfiltered request, and checking that the number of
83+
// samples in the response is the same as the number in the first response.
84+
final secondUnfilteredResult =
85+
await service.getPerfettoCpuSamples(isolateRef.id!);
86+
final secondUnfilteredTrace =
87+
Trace.fromBuffer(base64Decode(secondUnfilteredResult.samples!));
88+
expect(
89+
extractPerfSamplesFromTracePackets(secondUnfilteredTrace.packet).length,
90+
perfSamples.length,
91+
);
92+
},
93+
];
94+
95+
Future<void> main([args = const <String>[]]) => runIsolateTests(
96+
args,
97+
tests,
98+
'profile_startup_cli_flag_test.dart',
99+
testeeBefore: testeeMain,
100+
pauseOnExit: true,
101+
extraArgs: [
102+
'--profiler',
103+
'--profile-startup',
104+
'--profile-period=100',
105+
'--max-profile-depth=2',
106+
'--sample-buffer-duration=1',
107+
],
108+
);

runtime/bin/main_options.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ void Options::PrintUsage() {
216216
"--profile-microtasks\n"
217217
" Record information about each microtask. Information about completed\n"
218218
" microtasks will be written to the \"Microtask\" timeline stream.\n"
219+
"--profile-startup\n"
220+
" Make the profiler discard new samples once the profiler sample buffer is\n"
221+
" full. When this flag is not set, the profiler sample buffer is used as a\n"
222+
" ring buffer, meaning that once it is full, new samples start overwriting\n"
223+
" the oldest ones. This flag itself does not enable the profiler; the\n"
224+
" profiler must be enabled separately, e.g. with --profiler.\n"
219225
#endif // !defined(PRODUCT)
220226
"--version\n"
221227
" Print the VM version.\n"
@@ -516,6 +522,7 @@ bool Options::ProcessVMDebuggingOptions(const char* arg,
516522
V("--no-warn-on-pause-with-no-debugger", arg) \
517523
V("--timeline-streams", arg) \
518524
V("--timeline-recorder", arg) \
525+
V("--profile-startup", arg) \
519526
V("--enable-experiment", arg)
520527
HANDLE_DARTDEV_VM_DEBUG_OPTIONS(IS_DEBUG_OPTION, arg);
521528

runtime/vm/profiler.cc

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,15 @@ DEFINE_FLAG(
6363
"N seconds of samples at a given sample rate. If not provided, the "
6464
"default is ~4 seconds. Large values will greatly increase memory "
6565
"consumption.");
66+
DEFINE_FLAG(
67+
bool,
68+
profile_startup,
69+
false,
70+
"Make the profiler discard new samples once the profiler sample buffer is "
71+
"full. When this flag is not set, the profiler sample buffer is used as a "
72+
"ring buffer, meaning that once it is full, new samples start overwriting "
73+
"the oldest ones. This flag itself does not enable the profiler; the "
74+
"profiler must be enabled separately, e.g. with --profiler.");
6675

6776
// Include native stack dumping helpers into AOT compiler even in PRODUCT
6877
// mode. This allows to report more informative errors when gen_snapshot
@@ -740,17 +749,24 @@ SampleBlock* SampleBlockBuffer::ReserveSampleBlock() {
740749
i = (i + 1) % capacity;
741750
} while (i != start);
742751

743-
// No free blocks: try for completed block instead.
744-
i = start;
745-
do {
746-
SampleBlock* block = &blocks_[i];
747-
if (block->TryAllocateCompleted()) {
748-
return block;
749-
}
750-
i = (i + 1) % capacity;
751-
} while (i != start);
752+
if (FLAG_profile_startup) {
753+
// There are no free blocks and [FLAG_profile_startup] is set, so we stop
754+
// recording samples.
755+
return nullptr;
756+
} else {
757+
// There are no free blocks and [FLAG_profile_startup] is not set, so we
758+
// reuse a completed block if one is available.
759+
i = start;
760+
do {
761+
SampleBlock* block = &blocks_[i];
762+
if (block->TryAllocateCompleted()) {
763+
return block;
764+
}
765+
i = (i + 1) % capacity;
766+
} while (i != start);
752767

753-
return nullptr;
768+
return nullptr;
769+
}
754770
}
755771

756772
void SampleBlockBuffer::FreeCompletedBlocks() {

0 commit comments

Comments
 (0)