diff --git a/pkg/vm_service/test/get_http_profile_test.dart b/pkg/vm_service/test/get_http_profile_test.dart index a74d340dcdf..7892f770367 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 @@ Future hasValidHttpRequests(HttpProfile profile, String method) async { if (method == 'POST') { // add() was used expect( - [0, 1, 2], fullRequest.requestBody!, + [0, 1, 2], ); } else { // write() was used. diff --git a/sdk/lib/_http/http_impl.dart b/sdk/lib/_http/http_impl.dart index 6090f6f78c6..88ab38f75c4 100644 --- a/sdk/lib/_http/http_impl.dart +++ b/sdk/lib/_http/http_impl.dart @@ -24,19 +24,16 @@ abstract final class HttpProfiler { 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> serializeHttpProfileRequests( + int? updatedSince) { + return _profile.values + .where( + (e) => (updatedSince == null) || e.lastUpdateTime >= updatedSince, + ) + .map((e) => e.toJson(ref: true)) + .toList(); } } @@ -223,7 +220,7 @@ class _HttpProfileData { _updated(); } - Map toJson({bool ref = true}) { + Map toJson({required bool ref}) { return { 'type': '${ref ? '@' : ''}HttpProfileRequest', 'id': id, diff --git a/sdk/lib/developer/developer.dart b/sdk/lib/developer/developer.dart index efc2d26d581..36d22057949 100644 --- a/sdk/lib/developer/developer.dart +++ b/sdk/lib/developer/developer.dart @@ -80,6 +80,7 @@ import 'dart:convert'; 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 00000000000..a3a02800a83 --- /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> _developerProfilingData = >[]; + +/// 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 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> getHttpClientProfilingData() { + return UnmodifiableListView(_developerProfilingData); +} diff --git a/sdk/lib/io/network_profiling.dart b/sdk/lib/io/network_profiling.dart index 158accf83d1..abeb057cc25 100644 --- a/sdk/lib/io/network_profiling.dart +++ b/sdk/lib/io/network_profiling.dart @@ -11,6 +11,39 @@ const int _versionMinor = 0; 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> _createHttpProfileRequestFromProfileMap( + Map requestProfile, + {required bool ref}) async { + final responseData = requestProfile['responseData'] as Map; + + return { + '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>) + .expand((List i) => i) + .toList(), + if (!ref && responseData['endTime'] != null) + 'responseBody': + await (requestProfile['_responseBodyStream'] as Stream>) + .expand((List i) => i) + .toList(), + }; +} + @pragma('vm:entry-point', !const bool.fromEnvironment("dart.vm.product")) abstract class _NetworkProfiling { // Http relative RPCs @@ -43,7 +76,7 @@ abstract class _NetworkProfiling { } static Future _serviceExtensionHandler( - String method, Map parameters) { + String method, Map parameters) async { try { String responseJson; switch (method) { @@ -54,14 +87,25 @@ abstract class _NetworkProfiling { 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 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 @@ abstract class _NetworkProfiling { 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 @@ String _setHttpEnableTimelineLogging(Map parameters) { return _success(); } -String _getHttpProfileRequest(Map parameters) { +Future _getHttpProfileRequest(Map 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 parameters) {