Support the dart:developer timeline APIs in dart2js and DDC.
Exposes timeline events in the Chrome DevTools performance panel using the Web APIs performance.mark() and performance.measure(): https://developer.mozilla.org/en-US/docs/Web/API/Performance
CoreLibraryReviewExempt: Only change in sdk/lib is updating a comment.
Bug: https://github.com/flutter/devtools/issues/4652
Change-Id: I4f934bcffeb2920ffaf9b7b3a67fc5fc3b814294
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/310974
Reviewed-by: Stephen Adams <sra@google.com>
Commit-Queue: Elliott Brooks <elliottbrooks@google.com>
diff --git a/sdk/lib/_internal/js_dev_runtime/patch/developer_patch.dart b/sdk/lib/_internal/js_dev_runtime/patch/developer_patch.dart
index 3c13ae1..41e2673 100644
--- a/sdk/lib/_internal/js_dev_runtime/patch/developer_patch.dart
+++ b/sdk/lib/_internal/js_dev_runtime/patch/developer_patch.dart
@@ -12,6 +12,15 @@
import 'dart:convert' show json;
import 'dart:isolate';
+// These values must be kept in sync with developer/timeline.dart.
+const int _beginPatch = 1;
+const int _endPatch = 2;
+const int _asyncBeginPatch = 5;
+const int _asyncEndPatch = 7;
+const int _flowBeginPatch = 9;
+const int _flowStepPatch = 10;
+const int _flowEndPatch = 11;
+
var _issuedRegisterExtensionWarning = false;
var _issuedPostEventWarning = false;
final _developerSupportWarning = 'from dart:developer is only supported in '
@@ -140,26 +149,110 @@
@patch
bool _isDartStreamEnabled() {
- return false;
+ return true;
}
@patch
int _getTraceClock() {
- // TODO.
- return _clockValue++;
+ // Note: Use `millisecondsSinceEpoch` instead of `microsecondsSinceEpoch`
+ // because JS isn't able to hold the value of `microsecondsSinceEpoch` without
+ // rounding errors.
+ return DateTime.now().millisecondsSinceEpoch;
}
-int _clockValue = 0;
+int _taskId = 1;
@patch
int _getNextTaskId() {
- return 0;
+ return _taskId++;
+}
+
+bool _isBeginEvent(int type) => type == _beginPatch || type == _asyncBeginPatch;
+
+bool _isEndEvent(int type) => type == _endPatch || type == _asyncEndPatch;
+
+bool _isUnsupportedEvent(int type) =>
+ type == _flowBeginPatch || type == _flowEndPatch || type == _flowStepPatch;
+
+String _createEventName({
+ required int taskId,
+ required String name,
+ required bool isBeginEvent,
+ required bool isEndEvent,
+}) {
+ if (isBeginEvent) {
+ return '$taskId-$name-begin';
+ }
+ if (isEndEvent) {
+ return '$taskId-$name-end';
+ }
+ // Return only the name for events that don't need measurements:
+ return name;
+}
+
+Map<String, int> _eventNameToCount = {};
+
+String _postfixWithCount(String eventName) {
+ final count = _eventNameToCount[eventName];
+ if (count == null) return eventName;
+ return '$eventName-$count';
+}
+
+void _incrementEventCount(String eventName) {
+ final currentCount = _eventNameToCount[eventName] ?? 0;
+ _eventNameToCount[eventName] = currentCount + 1;
+}
+
+void _decrementEventCount(String eventName) {
+ if (!_eventNameToCount.containsKey(eventName)) return;
+
+ final newCount = _eventNameToCount[eventName]! - 1;
+ if (newCount <= 0) {
+ _eventNameToCount.remove(eventName);
+ } else {
+ _eventNameToCount[eventName] = newCount;
+ }
}
@patch
void _reportTaskEvent(
int taskId, int flowId, int type, String name, String argumentsAsJson) {
- // TODO.
+ // Ignore any unsupported events.
+ if (_isUnsupportedEvent(type)) return;
+
+ final isBeginEvent = _isBeginEvent(type);
+ final isEndEvent = _isEndEvent(type);
+ var currentEventName = _createEventName(
+ taskId: taskId,
+ name: name,
+ isBeginEvent: isBeginEvent,
+ isEndEvent: isEndEvent,
+ );
+ // Postfix the event name with the current count of events with that name. This
+ // guarantees that we are always measuring from the most recent begin event.
+ if (isBeginEvent) {
+ _incrementEventCount(currentEventName);
+ currentEventName = _postfixWithCount(currentEventName);
+ }
+ final markOptions = JS('', '{detail: JSON.parse(#)}', argumentsAsJson);
+
+ // Start by creating a mark event.
+ JS('', 'performance.mark(#, #)', currentEventName, markOptions);
+
+ // If it's an end event, then create a measurement from the most recent begin
+ // event with the same name.
+ if (isEndEvent) {
+ final beginEventName = _createEventName(
+ taskId: taskId, name: name, isBeginEvent: true, isEndEvent: false);
+ JS(
+ '',
+ 'performance.measure(#, #, #)',
+ name,
+ _postfixWithCount(beginEventName),
+ currentEventName,
+ );
+ _decrementEventCount(beginEventName);
+ }
}
@patch
diff --git a/sdk/lib/_internal/js_runtime/lib/developer_patch.dart b/sdk/lib/_internal/js_runtime/lib/developer_patch.dart
index da95d3c..bde309b 100644
--- a/sdk/lib/_internal/js_runtime/lib/developer_patch.dart
+++ b/sdk/lib/_internal/js_runtime/lib/developer_patch.dart
@@ -9,6 +9,15 @@
import 'dart:async' show Zone;
import 'dart:isolate';
+// These values must be kept in sync with developer/timeline.dart.
+const int _beginPatch = 1;
+const int _endPatch = 2;
+const int _asyncBeginPatch = 5;
+const int _asyncEndPatch = 7;
+const int _flowBeginPatch = 9;
+const int _flowStepPatch = 10;
+const int _flowEndPatch = 11;
+
@patch
@pragma('dart2js:tryInline')
bool debugger({bool when = true, String? message}) {
@@ -60,26 +69,110 @@
@patch
bool _isDartStreamEnabled() {
- return false;
+ return true;
}
@patch
int _getTraceClock() {
- // TODO.
- return _clockValue++;
+ // Note: Use `millisecondsSinceEpoch` instead of `microsecondsSinceEpoch`
+ // because JS isn't able to hold the value of `microsecondsSinceEpoch` without
+ // rounding errors.
+ return DateTime.now().millisecondsSinceEpoch;
}
-int _clockValue = 0;
+int _taskId = 1;
@patch
int _getNextTaskId() {
- return 0;
+ return _taskId++;
+}
+
+bool _isBeginEvent(int type) => type == _beginPatch || type == _asyncBeginPatch;
+
+bool _isEndEvent(int type) => type == _endPatch || type == _asyncEndPatch;
+
+bool _isUnsupportedEvent(int type) =>
+ type == _flowBeginPatch || type == _flowEndPatch || type == _flowStepPatch;
+
+String _createEventName({
+ required int taskId,
+ required String name,
+ required bool isBeginEvent,
+ required bool isEndEvent,
+}) {
+ if (isBeginEvent) {
+ return '$taskId-$name-begin';
+ }
+ if (isEndEvent) {
+ return '$taskId-$name-end';
+ }
+ // Return only the name for events that don't need measurements:
+ return name;
+}
+
+Map<String, int> _eventNameToCount = {};
+
+String _postfixWithCount(String eventName) {
+ final count = _eventNameToCount[eventName];
+ if (count == null) return eventName;
+ return '$eventName-$count';
+}
+
+void _incrementEventCount(String eventName) {
+ final currentCount = _eventNameToCount[eventName] ?? 0;
+ _eventNameToCount[eventName] = currentCount + 1;
+}
+
+void _decrementEventCount(String eventName) {
+ if (!_eventNameToCount.containsKey(eventName)) return;
+
+ final newCount = _eventNameToCount[eventName]! - 1;
+ if (newCount <= 0) {
+ _eventNameToCount.remove(eventName);
+ } else {
+ _eventNameToCount[eventName] = newCount;
+ }
}
@patch
void _reportTaskEvent(
int taskId, int flowId, int type, String name, String argumentsAsJson) {
- // TODO.
+ // Ignore any unsupported events.
+ if (_isUnsupportedEvent(type)) return;
+
+ final isBeginEvent = _isBeginEvent(type);
+ final isEndEvent = _isEndEvent(type);
+ var currentEventName = _createEventName(
+ taskId: taskId,
+ name: name,
+ isBeginEvent: isBeginEvent,
+ isEndEvent: isEndEvent,
+ );
+ // Postfix the event name with the current count of events with that name. This
+ // guarantees that we are always measuring from the most recent begin event.
+ if (isBeginEvent) {
+ _incrementEventCount(currentEventName);
+ currentEventName = _postfixWithCount(currentEventName);
+ }
+ final markOptions = JS('', '{detail: JSON.parse(#)}', argumentsAsJson);
+
+ // Start by creating a mark event.
+ JS('', 'performance.mark(#, #)', currentEventName, markOptions);
+
+ // If it's an end event, then create a measurement from the most recent begin
+ // event with the same name.
+ if (isEndEvent) {
+ final beginEventName = _createEventName(
+ taskId: taskId, name: name, isBeginEvent: true, isEndEvent: false);
+ JS(
+ '',
+ 'performance.measure(#, #, #)',
+ name,
+ _postfixWithCount(beginEventName),
+ currentEventName,
+ );
+ _decrementEventCount(beginEventName);
+ }
}
@patch
diff --git a/sdk/lib/developer/timeline.dart b/sdk/lib/developer/timeline.dart
index 84923ae..428a1ec 100644
--- a/sdk/lib/developer/timeline.dart
+++ b/sdk/lib/developer/timeline.dart
@@ -17,7 +17,9 @@
typedef Future TimelineAsyncFunction();
// These values must be kept in sync with the enum "EventType" in
-// runtime/vm/timeline.h.
+// runtime/vm/timeline.h, along with the JS-specific implementations in:
+// - _internal/js_runtime/lib/developer_patch.dart
+// - _internal/js_dev_runtime/patch/developer_patch.dart
const int _begin = 1;
const int _end = 2;
const int _instant = 4;
diff --git a/tests/lib/lib.status b/tests/lib/lib.status
index 0fe390a..991e72b 100644
--- a/tests/lib/lib.status
+++ b/tests/lib/lib.status
@@ -26,6 +26,7 @@
isolate/issue_24243_parent_isolate_test: Skip # Requires checked mode
[ $runtime == d8 ]
+developer/timeline_recorders_test: SkipByDesign # https://bugs.chromium.org/p/v8/issues/detail?id=14129
js/export/static_interop_mock/proto_test: SkipByDesign # Uses dart:html.
js/js_util/javascriptobject_extensions_test: SkipByDesign # Uses dart:html.
js/static_interop_test/constants_test: SkipByDesign # Uses dart:html.