Skip to content

Commit 1d69b0b

Browse files
derekxu16Commit Queue
authored and
Commit Queue
committedApr 14, 2025·
[VM] Add automatic detection of timers that are significantly overdue
This change makes it so that when the VM detects that a timer is at least 100 ms overdue, it sends a warning in a VM Service event on the 'Timer' stream. TEST=Built an Android Flutter app with a modified Engine that contained a `sleep` call in `eventhandler_linux.cc` and confirmed that the warning event got sent, built an iOS Flutter app with a modified Engine that contained a `sleep` call in `eventhandler_macos.cc` and verified that the warning event got sent, pkg/vm_service/test/overdue_timer_detection_test CoreLibraryReviewExempt: This CL does not include any core library API changes, only VM Service implementation changes. Change-Id: Ie8db047116b7f63cfb5413f763eaf56c7bdd6975 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/409500 Reviewed-by: Ben Konyi <bkonyi@google.com> Commit-Queue: Derek Xu <derekx@google.com>
1 parent ed54a42 commit 1d69b0b

17 files changed

+223
-20
lines changed
 

‎pkg/vm_service/CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 15.0.1
2+
- Update to version `4.17` of the spec.
3+
- Add `Timer` stream.
4+
- Add `TimerSignificantlyOverdue` event kind.
5+
- Add `details` property to `Event`.
6+
17
## 15.0.0
28
- Update type of `CodeRef.function` from `FuncRef` to `dynamic` to allow for `NativeFunction`
39
functions ([flutter/devtools #8567]).
+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=4.16
1+
version=4.17

‎pkg/vm_service/lib/src/vm_service.dart

+19-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export 'snapshot_graph.dart'
2727
HeapSnapshotObjectNoData,
2828
HeapSnapshotObjectNullData;
2929

30-
const String vmServiceVersion = '4.16.0';
30+
const String vmServiceVersion = '4.17.0';
3131

3232
/// @optional
3333
const String optional = 'optional';
@@ -385,6 +385,9 @@ class VmService {
385385
Stream<Event> get onHeapSnapshotEvent =>
386386
_getEventController('HeapSnapshot').stream;
387387

388+
// TimerSignificantlyOverdue
389+
Stream<Event> get onTimerEvent => _getEventController('Timer').stream;
390+
388391
// WriteEvent
389392
Stream<Event> get onStdoutEvent => _getEventController('Stdout').stream;
390393

@@ -1830,6 +1833,7 @@ class VmService {
18301833
/// Logging | Logging
18311834
/// Service | ServiceRegistered, ServiceUnregistered
18321835
/// HeapSnapshot | HeapSnapshot
1836+
/// Timer | TimerSignificantlyOverdue
18331837
///
18341838
/// Additionally, some embedders provide the `Stdout` and `Stderr` streams.
18351839
/// These streams allow the client to subscribe to writes to stdout and
@@ -2292,6 +2296,7 @@ abstract class EventStreams {
22922296
static const String kLogging = 'Logging';
22932297
static const String kService = 'Service';
22942298
static const String kHeapSnapshot = 'HeapSnapshot';
2299+
static const String kTimer = 'Timer';
22952300
static const String kStdout = 'Stdout';
22962301
static const String kStderr = 'Stderr';
22972302
}
@@ -3950,6 +3955,16 @@ class Event extends Response {
39503955
@optional
39513956
LogRecord? logRecord;
39523957

3958+
/// Details about this event.
3959+
///
3960+
/// For events of kind TimerSignifcantlyOverdue, this is a message stating how
3961+
/// many milliseconds late the timer fired, and giving possible reasons for
3962+
/// why it fired late.
3963+
///
3964+
/// Only provided for events of kind TimerSignificantlyOverdue.
3965+
@optional
3966+
String? details;
3967+
39533968
/// The service identifier.
39543969
///
39553970
/// This is provided for the event kinds:
@@ -4035,6 +4050,7 @@ class Event extends Response {
40354050
this.status,
40364051
this.reloadFailureReason,
40374052
this.logRecord,
4053+
this.details,
40384054
this.service,
40394055
this.method,
40404056
this.alias,
@@ -4085,6 +4101,7 @@ class Event extends Response {
40854101
reloadFailureReason = json['reloadFailureReason'];
40864102
logRecord = createServiceObject(json['logRecord'], const ['LogRecord'])
40874103
as LogRecord?;
4104+
details = json['details'];
40884105
service = json['service'];
40894106
method = json['method'];
40904107
alias = json['alias'];
@@ -4143,6 +4160,7 @@ class Event extends Response {
41434160
'reloadFailureReason': reloadFailureReasonValue,
41444161
if (logRecord?.toJson() case final logRecordValue?)
41454162
'logRecord': logRecordValue,
4163+
if (details case final detailsValue?) 'details': detailsValue,
41464164
if (service case final serviceValue?) 'service': serviceValue,
41474165
if (method case final methodValue?) 'method': methodValue,
41484166
if (alias case final aliasValue?) 'alias': aliasValue,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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:async';
6+
import 'dart:io' show sleep;
7+
8+
import 'package:test/test.dart';
9+
import 'package:vm_service/vm_service.dart';
10+
11+
import 'common/service_test_common.dart';
12+
import 'common/test_helper.dart';
13+
14+
Future<void> testeeMain() async {
15+
final completer = Completer<void>();
16+
late final Timer t;
17+
t = Timer(
18+
const Duration(milliseconds: 100),
19+
() {
20+
t.cancel();
21+
completer.complete();
22+
},
23+
);
24+
25+
// Sleep for 201 ms to force [t] to fire at least 100 ms late. This allows us
26+
// to expect to receive at least one 'TimerSignificantlyOverdue' event in
27+
// [tests] below, because a 'TimerSignificantlyOverdue' event should be fired
28+
// whenever a timer is identified to be at least 100 ms overdue.
29+
sleep(const Duration(milliseconds: 201));
30+
await completer.future;
31+
}
32+
33+
final tests = <IsolateTest>[
34+
hasPausedAtStart,
35+
(VmService service, IsolateRef isolateRef) async {
36+
final completer = Completer<void>();
37+
late final StreamSubscription<Event> sub;
38+
sub = service.onTimerEvent.listen((Event event) async {
39+
if (event.kind == 'TimerSignificantlyOverdue') {
40+
final detailsRegex = RegExp(
41+
r'A timer should have fired (\d+) ms ago, but just fired now.',
42+
);
43+
final millisecondsOverdueAsString =
44+
detailsRegex.firstMatch(event.details!)!.group(1)!;
45+
expect(
46+
int.parse(millisecondsOverdueAsString),
47+
greaterThanOrEqualTo(100),
48+
);
49+
await sub.cancel();
50+
completer.complete();
51+
}
52+
});
53+
await service.streamListen(EventStreams.kTimer);
54+
55+
await service.resume(isolateRef.id!);
56+
await completer.future;
57+
},
58+
];
59+
60+
Future<void> main([args = const <String>[]]) => runIsolateTests(
61+
args,
62+
tests,
63+
'overdue_timer_detection_test.dart',
64+
testeeConcurrent: testeeMain,
65+
pauseOnStart: true,
66+
);

‎pkg/vm_service_interface/lib/src/vm_service_interface.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import 'service_extension_registry.dart';
1717

1818
export 'service_extension_registry.dart' show ServiceExtensionRegistry;
1919

20-
const String vmServiceVersion = '4.16.0';
20+
const String vmServiceVersion = '4.17.0';
2121

2222
/// A class representation of the Dart VM Service Protocol.
2323
abstract interface class VmServiceInterface {
@@ -1292,6 +1292,7 @@ abstract interface class VmServiceInterface {
12921292
/// Logging | Logging
12931293
/// Service | ServiceRegistered, ServiceUnregistered
12941294
/// HeapSnapshot | HeapSnapshot
1295+
/// Timer | TimerSignificantlyOverdue
12951296
///
12961297
/// Additionally, some embedders provide the `Stdout` and `Stderr` streams.
12971298
/// These streams allow the client to subscribe to writes to stdout and
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Detecting Bugs in the Implementation of Timers
2+
3+
When an app is connected to DevTools, a message will appear in the Logging view
4+
of DevTools whenever a timer in the app fires at least 100 ms late.
5+
6+
<!-- // TODO(derekxu16): Insert a screenshot of DevTools displaying these messages. -->
7+
8+
As the messages in the screenshot state, the appearance of these messages does
9+
not necessarily mean that there is a bug in the implementation of timers. Timers
10+
can be blocked from firing for other reasons. A stretch of uninterruptible
11+
synchronous operations can block asynchronous operations, or an app can be
12+
frozen by the OS to conserve resources. When "late timer" messages appear, these
13+
other causes must be ruled out before before suspecting that there is a bug in
14+
the implementation of timers.
15+
16+
Be wary of the fact that it is possible for a bug in the implementation of
17+
timers to manifest only by increasing the delay length of timers that would have
18+
also been delayed under normal circumstances. For example,
19+
[this bug in the implementation of timers on Android](https://github.com/dart-lang/sdk/issues/54868)
20+
only manifested after a given Android app was put into and then taken out of the
21+
Android cached apps freezer. One would have had to join the information logged
22+
in DevTools with information logged by the Android platform to discover this
23+
bug. The following is an explanation of how this could have been done.
24+
25+
Consider an app that was put into and taken out of the cached apps freezer. The
26+
times at which the app was put into and taken out of the cached apps freezer
27+
will be available in `logcat`.
28+
[This link](https://source.android.com/docs/core/perf/cached-apps-freezer#testing-the-apps-freezer)
29+
explains how to find those times. Let those times be `f_in` and `f_out`. There
30+
will be messages in DevTools that say "A timer was supposed to fire `x_i` ms
31+
ago, but just fired now...". The time at which each message was logged will also
32+
be shown in DevTools; let these time be `m_i`. If a message exists such that
33+
(`f_in` <= `m_i` - `x_i` <= `f_out`) AND (`x_i` >= `f_out` - `f_in`), and other
34+
asynchronous operations blockers can be ruled out, then it means that there is a
35+
bug in the implementation of timers.

‎runtime/lib/isolate.cc

+12
Original file line numberDiff line numberDiff line change
@@ -1392,4 +1392,16 @@ DEFINE_NATIVE_ENTRY(TransferableTypedData_materialize, 0, 1) {
13921392
return typed_data.ptr();
13931393
}
13941394

1395+
DEFINE_NATIVE_ENTRY(Timer_postTimerEvent, 0, 1) {
1396+
#if !defined(PRODUCT)
1397+
GET_NON_NULL_NATIVE_ARGUMENT(Integer, milliseconds_overdue,
1398+
arguments->NativeArgAt(0));
1399+
// |milliseconds_overdue| can get truncated on 32-bit platforms, but in
1400+
// practice, it should never get close to |INT_MAX|.
1401+
Service::SendTimerEvent(isolate,
1402+
static_cast<intptr_t>(milliseconds_overdue.Value()));
1403+
#endif // !defined(PRODUCT)
1404+
return Object::null();
1405+
}
1406+
13951407
} // namespace dart

‎runtime/observatory/tests/service/get_version_rpc_test.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ var tests = <VMTest>[
1212
final result = await vm.invokeRpcNoUpgrade('getVersion', {});
1313
expect(result['type'], 'Version');
1414
expect(result['major'], 4);
15-
expect(result['minor'], 16);
15+
expect(result['minor'], 17);
1616
expect(result['_privateMajor'], 0);
1717
expect(result['_privateMinor'], 0);
1818
},

‎runtime/vm/bootstrap_natives.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,8 @@ namespace dart {
312312
V(DartApiDLMinorVersion, 0) \
313313
V(DartNativeApiFunctionPointer, 1) \
314314
V(TransferableTypedData_factory, 2) \
315-
V(TransferableTypedData_materialize, 1)
315+
V(TransferableTypedData_materialize, 1) \
316+
V(Timer_postTimerEvent, 1)
316317

317318
// List of bootstrap native entry points used in the dart:mirror library.
318319
#define MIRRORS_BOOTSTRAP_NATIVE_LIST(V) \

‎runtime/vm/service.cc

+16-8
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@ StreamInfo Service::gc_stream("GC");
480480
StreamInfo Service::echo_stream("_Echo");
481481
StreamInfo Service::heapsnapshot_stream("HeapSnapshot");
482482
StreamInfo Service::logging_stream("Logging");
483+
StreamInfo Service::timer_stream("Timer");
483484
StreamInfo Service::extension_stream("Extension");
484485
StreamInfo Service::timeline_stream("Timeline");
485486
StreamInfo Service::profiler_stream("Profiler");
@@ -490,11 +491,12 @@ intptr_t Service::dart_library_kernel_len_ = 0;
490491
// Keep streams_ in sync with the protected streams in
491492
// lib/developer/extension.dart
492493
static StreamInfo* const streams_[] = {
493-
&Service::vm_stream, &Service::isolate_stream,
494-
&Service::debug_stream, &Service::gc_stream,
495-
&Service::echo_stream, &Service::heapsnapshot_stream,
496-
&Service::logging_stream, &Service::extension_stream,
497-
&Service::timeline_stream, &Service::profiler_stream,
494+
&Service::vm_stream, &Service::isolate_stream,
495+
&Service::debug_stream, &Service::gc_stream,
496+
&Service::echo_stream, &Service::heapsnapshot_stream,
497+
&Service::logging_stream, &Service::timer_stream,
498+
&Service::extension_stream, &Service::timeline_stream,
499+
&Service::profiler_stream,
498500
};
499501

500502
bool Service::ListenStream(const char* stream_id,
@@ -5106,12 +5108,18 @@ void Service::SendLogEvent(Isolate* isolate,
51065108
Service::HandleEvent(&event);
51075109
}
51085110

5111+
void Service::SendTimerEvent(Isolate* isolate, intptr_t milliseconds_overdue) {
5112+
if (!Service::timer_stream.enabled()) {
5113+
return;
5114+
}
5115+
ServiceEvent event(isolate, ServiceEvent::kTimerSignificantlyOverdue);
5116+
event.set_milliseconds_overdue(milliseconds_overdue);
5117+
Service::HandleEvent(&event);
5118+
}
5119+
51095120
void Service::SendExtensionEvent(Isolate* isolate,
51105121
const String& event_kind,
51115122
const String& event_data) {
5112-
if (!Service::extension_stream.enabled()) {
5113-
return;
5114-
}
51155123
ServiceEvent::ExtensionEvent extension_event;
51165124
extension_event.event_kind = &event_kind;
51175125
extension_event.event_data = &event_data;

‎runtime/vm/service.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
namespace dart {
2020

2121
#define SERVICE_PROTOCOL_MAJOR_VERSION 4
22-
#define SERVICE_PROTOCOL_MINOR_VERSION 16
22+
#define SERVICE_PROTOCOL_MINOR_VERSION 17
2323

2424
class Array;
2525
class EmbedderServiceHandler;
@@ -178,6 +178,9 @@ class Service : public AllStatic {
178178
const Object& error,
179179
const Instance& stack_trace);
180180

181+
// Sends an event of kind |kTimerSignificantlyOverdue|.
182+
static void SendTimerEvent(Isolate* isolate, intptr_t milliseconds_overdue);
183+
181184
static void SendExtensionEvent(Isolate* isolate,
182185
const String& event_kind,
183186
const String& event_data);
@@ -213,6 +216,7 @@ class Service : public AllStatic {
213216
static StreamInfo echo_stream;
214217
static StreamInfo heapsnapshot_stream;
215218
static StreamInfo logging_stream;
219+
static StreamInfo timer_stream;
216220
static StreamInfo extension_stream;
217221
static StreamInfo timeline_stream;
218222
static StreamInfo profiler_stream;

‎runtime/vm/service/service.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Dart VM Service Protocol 4.16
1+
# Dart VM Service Protocol 4.17
22

33
> Please post feedback to the [observatory-discuss group][discuss-list]
44
@@ -1892,6 +1892,7 @@ Timeline | TimelineEvents, TimelineStreamsSubscriptionUpdate
18921892
Logging | Logging
18931893
Service | ServiceRegistered, ServiceUnregistered
18941894
HeapSnapshot | HeapSnapshot
1895+
Timer | TimerSignificantlyOverdue
18951896

18961897
Additionally, some embedders provide the _Stdout_ and _Stderr_
18971898
streams. These streams allow the client to subscribe to writes to
@@ -2602,6 +2603,17 @@ class Event extends Response {
26022603
// This is provided for the Logging event.
26032604
LogRecord logRecord [optional];
26042605
2606+
2607+
// Details about this event.
2608+
//
2609+
// For events of kind TimerSignifcantlyOverdue, this is a message stating how
2610+
// many milliseconds late the timer fired, and giving possible reasons for why
2611+
// it fired late.
2612+
//
2613+
// Only provided for events of kind TimerSignificantlyOverdue.
2614+
string details [optional];
2615+
2616+
26052617
// The service identifier.
26062618
//
26072619
// This is provided for the event kinds:
@@ -4981,5 +4993,6 @@ version | comments
49814993
4.14 | Added `Finalizer`, `NativeFinalizer`, and `FinalizerEntry`.
49824994
4.15 | Added `closureReceiver` property to `@Instance` and `Instance`.
49834995
4.16 | Added `reloadFailureReason` property to `Event`. Added `createIdZone`, `deleteIdZone`, and `invalidateIdZone` RPCs. Added optional `idZoneId` parameter to `evaluate`, `evaluateInFrame`, `getInboundReferences`, `getInstances`, `getInstancesAsList`, `getObject`, `getRetainingPath`, `getStack`, and `invoke` RPCs.
4996+
4.17 | Added `Timer` stream, added `TimerSignificantlyOverdue` event kind, and added `details` property to `Event`.
49844997

49854998
[discuss-list]: https://groups.google.com/a/dartlang.org/forum/#!forum/observatory-discuss

0 commit comments

Comments
 (0)
Please sign in to comment.