[dart:developer][VM/Service] Add APIs to dart:developer for recording HTTP profiling information, and for later retrieving that information

The APIs mentioned above are `addHttpClientProfilingData` and
`getHttpClientProfilingData`.

This CL also makes it so that profiling information recorded using
`addHttpClientProfilingData` is included in dart:io service extension
responses.

TEST= pkg/vm_service/test/get_http_profile_test.dart

Change-Id: I892a7a8485369bb92cbb0c086b93498bcec25d5f
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/341440
Reviewed-by: Sigmund Cherem <sigmund@google.com>
Commit-Queue: Derek Xu <derekx@google.com>
Reviewed-by: Ömer Ağacan <omersa@google.com>
Reviewed-by: Siva Annamalai <asiva@google.com>
Reviewed-by: Leaf Petersen <leafp@google.com>
Reviewed-by: Ben Konyi <bkonyi@google.com>
diff --git a/pkg/vm_service/test/get_http_profile_test.dart b/pkg/vm_service/test/get_http_profile_test.dart
index a74d340..7892f77 100644
--- a/pkg/vm_service/test/get_http_profile_test.dart
+++ b/pkg/vm_service/test/get_http_profile_test.dart
@@ -242,8 +242,8 @@
           if (method == 'POST') {
             // add() was used
             expect(
-              <int>[0, 1, 2],
               fullRequest.requestBody!,
+              <int>[0, 1, 2],
             );
           } else {
             // write() was used.
diff --git a/sdk/lib/_http/http_impl.dart b/sdk/lib/_http/http_impl.dart
index 6090f6f..88ab38f 100644
--- a/sdk/lib/_http/http_impl.dart
+++ b/sdk/lib/_http/http_impl.dart
@@ -24,19 +24,16 @@
 
   static void clear() => _profile.clear();
 
-  static String toJson(int? updatedSince) {
-    return json.encode({
-      'type': _kType,
-      'timestamp': DateTime.now().microsecondsSinceEpoch,
-      'requests': [
-        for (final request in _profile.values.where(
-          (e) {
-            return (updatedSince == null) || e.lastUpdateTime >= updatedSince;
-          },
-        ))
-          request.toJson(),
-      ],
-    });
+  /// Returns a list of Maps, where each map conforms to the @HttpProfileRequest
+  /// type defined in the dart:io service extension spec.
+  static List<Map<String, dynamic>> serializeHttpProfileRequests(
+      int? updatedSince) {
+    return _profile.values
+        .where(
+          (e) => (updatedSince == null) || e.lastUpdateTime >= updatedSince,
+        )
+        .map((e) => e.toJson(ref: true))
+        .toList();
   }
 }
 
@@ -223,7 +220,7 @@
     _updated();
   }
 
-  Map<String, dynamic> toJson({bool ref = true}) {
+  Map<String, dynamic> toJson({required bool ref}) {
     return <String, dynamic>{
       'type': '${ref ? '@' : ''}HttpProfileRequest',
       'id': id,
diff --git a/sdk/lib/developer/developer.dart b/sdk/lib/developer/developer.dart
index efc2d26..36d2205 100644
--- a/sdk/lib/developer/developer.dart
+++ b/sdk/lib/developer/developer.dart
@@ -80,6 +80,7 @@
 import 'dart:isolate' show Isolate, RawReceivePort, SendPort;
 
 part 'extension.dart';
+part 'http_profiling.dart';
 part 'profiler.dart';
 part 'service.dart';
 part 'timeline.dart';
diff --git a/sdk/lib/developer/http_profiling.dart b/sdk/lib/developer/http_profiling.dart
new file mode 100644
index 0000000..a3a0280
--- /dev/null
+++ b/sdk/lib/developer/http_profiling.dart
@@ -0,0 +1,28 @@
+// Copyright (c) 2024, 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.
+
+part of dart.developer;
+
+// package:http_profile adds HTTP profiling information to this list by using
+// the [addHttpClientProfilingData] API below.
+List<Map<String, dynamic>> _developerProfilingData = <Map<String, dynamic>>[];
+
+/// Records the data associated with an HTTP request for profiling purposes.
+///
+/// This function should never be called directly. Instead, use
+/// [package:http_profile](https://pub.dev/packages/http_profile).
+@Since('3.4')
+void addHttpClientProfilingData(Map<String, dynamic> requestProfile) {
+  _developerProfilingData.add(requestProfile);
+  requestProfile['id'] = 'from_package/${_developerProfilingData.length}';
+}
+
+/// Returns the data added through [addHttpClientProfilingData].
+///
+/// This function is only meant for use by networking profilers and the format
+/// of the returned data may change over time.
+@Since('3.4')
+List<Map<String, dynamic>> getHttpClientProfilingData() {
+  return UnmodifiableListView(_developerProfilingData);
+}
diff --git a/sdk/lib/io/network_profiling.dart b/sdk/lib/io/network_profiling.dart
index 158accf..abeb057 100644
--- a/sdk/lib/io/network_profiling.dart
+++ b/sdk/lib/io/network_profiling.dart
@@ -11,6 +11,39 @@
 const String _tcpSocket = 'tcp';
 const String _udpSocket = 'udp';
 
+// Creates a Map conforming to the HttpProfileRequest type defined in the
+// dart:io service extension spec from an element of dart:developer's
+// [_developerProfilingData].
+Future<Map<String, dynamic>> _createHttpProfileRequestFromProfileMap(
+    Map<String, dynamic> requestProfile,
+    {required bool ref}) async {
+  final responseData = requestProfile['responseData'] as Map<String, dynamic>;
+
+  return <String, dynamic>{
+    'type': '${ref ? '@' : ''}HttpProfileRequest',
+    'id': requestProfile['id']!,
+    'isolateId': requestProfile['isolateId']!,
+    'method': requestProfile['requestMethod']!,
+    'uri': requestProfile['requestUri']!,
+    'events': requestProfile['events']!,
+    'startTime': requestProfile['requestStartTimestamp']!,
+    if (requestProfile['requestEndTimestamp'] != null)
+      'endTime': requestProfile['requestEndTimestamp'],
+    'request': requestProfile['requestData']!,
+    'response': responseData,
+    if (!ref && requestProfile['requestEndTimestamp'] != null)
+      'requestBody':
+          await (requestProfile['_requestBodyStream'] as Stream<List<int>>)
+              .expand((List<int> i) => i)
+              .toList(),
+    if (!ref && responseData['endTime'] != null)
+      'responseBody':
+          await (requestProfile['_responseBodyStream'] as Stream<List<int>>)
+              .expand((List<int> i) => i)
+              .toList(),
+  };
+}
+
 @pragma('vm:entry-point', !const bool.fromEnvironment("dart.vm.product"))
 abstract class _NetworkProfiling {
   // Http relative RPCs
@@ -43,7 +76,7 @@
   }
 
   static Future<ServiceExtensionResponse> _serviceExtensionHandler(
-      String method, Map<String, String> parameters) {
+      String method, Map<String, String> parameters) async {
     try {
       String responseJson;
       switch (method) {
@@ -54,14 +87,25 @@
           responseJson = _getHttpEnableTimelineLogging();
           break;
         case _kGetHttpProfileRPC:
-          responseJson = HttpProfiler.toJson(
-            parameters.containsKey('updatedSince')
-                ? int.tryParse(parameters['updatedSince']!)
-                : null,
-          );
+          final updatedSince = parameters.containsKey('updatedSince')
+              ? int.tryParse(parameters['updatedSince']!)
+              : null;
+          responseJson = json.encode({
+            'type': 'HttpProfile',
+            'timestamp': DateTime.now().microsecondsSinceEpoch,
+            'requests': [
+              ...HttpProfiler.serializeHttpProfileRequests(updatedSince),
+              ...await Future.wait(getHttpClientProfilingData()
+                  .where((final Map<String, dynamic> p) =>
+                      updatedSince == null ||
+                      (p['_lastUpdateTime'] as int) >= updatedSince)
+                  .map((p) =>
+                      _createHttpProfileRequestFromProfileMap(p, ref: true)))
+            ],
+          });
           break;
         case _kGetHttpProfileRequestRPC:
-          responseJson = _getHttpProfileRequest(parameters);
+          responseJson = await _getHttpProfileRequest(parameters);
           break;
         case _kClearHttpProfileRPC:
           HttpProfiler.clear();
@@ -80,22 +124,16 @@
           responseJson = getVersion();
           break;
         default:
-          return Future.value(
-            ServiceExtensionResponse.error(
-              ServiceExtensionResponse.extensionError,
-              'Method $method does not exist',
-            ),
+          return ServiceExtensionResponse.error(
+            ServiceExtensionResponse.extensionError,
+            'Method $method does not exist',
           );
       }
-      return Future.value(
-        ServiceExtensionResponse.result(responseJson),
-      );
+      return ServiceExtensionResponse.result(responseJson);
     } catch (errorMessage) {
-      return Future.value(
-        ServiceExtensionResponse.error(
-          ServiceExtensionResponse.invalidParams,
-          errorMessage.toString(),
-        ),
+      return ServiceExtensionResponse.error(
+        ServiceExtensionResponse.invalidParams,
+        errorMessage.toString(),
       );
     }
   }
@@ -134,19 +172,26 @@
   return _success();
 }
 
-String _getHttpProfileRequest(Map<String, String> parameters) {
+Future<String> _getHttpProfileRequest(Map<String, String> parameters) async {
   if (!parameters.containsKey('id')) {
     throw _missingArgument('id');
   }
-  String id = parameters['id']!;
+  final id = parameters['id']!;
+  final request;
+  if (id.startsWith('from_package/')) {
+    final profileMap = getHttpClientProfilingData()
+        .elementAtOrNull(int.parse(id.substring('from_package/'.length)) - 1);
+    request = profileMap == null
+        ? null
+        : await _createHttpProfileRequestFromProfileMap(profileMap, ref: false);
+  } else {
+    request = HttpProfiler.getHttpProfileRequest(id)?.toJson(ref: false);
+  }
 
-  final request = HttpProfiler.getHttpProfileRequest(id);
   if (request == null) {
     throw "Unable to find request with id: '$id'";
   }
-  return json.encode(
-    request.toJson(ref: false),
-  );
+  return json.encode(request);
 }
 
 String _socketProfilingEnabled(Map<String, String> parameters) {