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.