| // Copyright (c) 2019, 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:io"; |
| |
| // TODO(bkonyi): refactor into io_resource_info.dart |
| const int _versionMajor = 4; |
| 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`. |
| Map<String, Object?> _createHttpProfileRequestFromProfileMap( |
| Map<String, dynamic> requestProfile, { |
| required bool ref, |
| }) { |
| final responseData = requestProfile['responseData'] as Map<String, dynamic>; |
| |
| 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': requestProfile['requestBodyBytes']!, |
| if (!ref && responseData['endTime'] != null) |
| 'responseBody': requestProfile['responseBodyBytes']!, |
| }; |
| } |
| |
| @pragma('vm:entry-point', !bool.fromEnvironment("dart.vm.product")) |
| abstract class _NetworkProfiling { |
| // Http relative RPCs |
| static const _kHttpEnableTimelineLogging = |
| 'ext.dart.io.httpEnableTimelineLogging'; |
| static const _kGetHttpProfileRPC = 'ext.dart.io.getHttpProfile'; |
| static const _kGetHttpProfileRequestRPC = 'ext.dart.io.getHttpProfileRequest'; |
| static const _kClearHttpProfileRPC = 'ext.dart.io.clearHttpProfile'; |
| // Socket relative RPCs |
| static const _kClearSocketProfileRPC = 'ext.dart.io.clearSocketProfile'; |
| static const _kGetSocketProfileRPC = 'ext.dart.io.getSocketProfile'; |
| static const _kSocketProfilingEnabledRPC = |
| 'ext.dart.io.socketProfilingEnabled'; |
| |
| // TODO(zichangguo): This version number represents the version of service |
| // extension of dart:io. Consider moving this out of web profiler class, |
| // if more methods added to dart:io, |
| static const _kGetVersionRPC = 'ext.dart.io.getVersion'; |
| |
| @pragma('vm:entry-point', !bool.fromEnvironment("dart.vm.product")) |
| static void _registerServiceExtension() { |
| registerExtension(_kHttpEnableTimelineLogging, _serviceExtensionHandler); |
| registerExtension(_kGetSocketProfileRPC, _serviceExtensionHandler); |
| registerExtension(_kSocketProfilingEnabledRPC, _serviceExtensionHandler); |
| registerExtension(_kClearSocketProfileRPC, _serviceExtensionHandler); |
| registerExtension(_kGetVersionRPC, _serviceExtensionHandler); |
| registerExtension(_kGetHttpProfileRPC, _serviceExtensionHandler); |
| registerExtension(_kGetHttpProfileRequestRPC, _serviceExtensionHandler); |
| registerExtension(_kClearHttpProfileRPC, _serviceExtensionHandler); |
| } |
| |
| // Note this function only returns a `Future` because that is required by the |
| // signature of `registerExtension`. We might be able to change the signature |
| // of ServiceExtensionHandler to use `FutureOr` instead of `Future`. |
| static Future<ServiceExtensionResponse> _serviceExtensionHandler( |
| String method, |
| Map<String, String> parameters, |
| ) async { |
| try { |
| String responseJson; |
| switch (method) { |
| case _kHttpEnableTimelineLogging: |
| if (parameters.containsKey('enabled')) { |
| _setHttpEnableTimelineLogging(parameters); |
| } |
| responseJson = _getHttpEnableTimelineLogging(); |
| break; |
| case _kGetHttpProfileRPC: |
| final updatedSince = parameters.containsKey('updatedSince') |
| ? int.tryParse(parameters['updatedSince']!) |
| : null; |
| responseJson = json.encode({ |
| 'type': 'HttpProfile', |
| 'timestamp': DateTime.now().microsecondsSinceEpoch, |
| 'requests': [ |
| ...HttpProfiler.serializeHttpProfileRequests(updatedSince), |
| ...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); |
| break; |
| case _kClearHttpProfileRPC: |
| HttpProfiler.clear(); |
| responseJson = _success(); |
| break; |
| case _kGetSocketProfileRPC: |
| responseJson = _SocketProfile.toJson(); |
| break; |
| case _kSocketProfilingEnabledRPC: |
| responseJson = _socketProfilingEnabled(parameters); |
| break; |
| case _kClearSocketProfileRPC: |
| responseJson = _SocketProfile.clear(); |
| break; |
| case _kGetVersionRPC: |
| responseJson = getVersion(); |
| break; |
| default: |
| return ServiceExtensionResponse.error( |
| ServiceExtensionResponse.extensionError, |
| 'Method $method does not exist', |
| ); |
| } |
| return ServiceExtensionResponse.result(responseJson); |
| } catch (errorMessage) { |
| return ServiceExtensionResponse.error( |
| ServiceExtensionResponse.invalidParams, |
| errorMessage.toString(), |
| ); |
| } |
| } |
| |
| static String getVersion() => json.encode({ |
| 'type': 'Version', |
| 'major': _versionMajor, |
| 'minor': _versionMinor, |
| }); |
| } |
| |
| String _success() => json.encode({'type': 'Success'}); |
| |
| String _invalidArgument(String argument, dynamic value) => |
| "Value for parameter '$argument' is not valid: $value"; |
| |
| String _missingArgument(String argument) => "Parameter '$argument' is required"; |
| |
| String _getHttpEnableTimelineLogging() => json.encode({ |
| 'type': 'HttpTimelineLoggingState', |
| 'enabled': HttpClient.enableTimelineLogging, |
| }); |
| |
| String _setHttpEnableTimelineLogging(Map<String, String> parameters) { |
| const String kEnabled = 'enabled'; |
| if (!parameters.containsKey(kEnabled)) { |
| throw _missingArgument(kEnabled); |
| } |
| final enable = parameters[kEnabled]!.toLowerCase(); |
| if (enable != 'true' && enable != 'false') { |
| throw _invalidArgument(kEnabled, enable); |
| } |
| HttpClient.enableTimelineLogging = enable == 'true'; |
| return _success(); |
| } |
| |
| String _getHttpProfileRequest(Map<String, String> parameters) { |
| final id = parameters['id']; |
| if (id == null) { |
| throw _missingArgument('id'); |
| } |
| final Map<String, Object?>? request; |
| if (id.startsWith('from_package/')) { |
| final profileMap = getHttpClientProfilingData().elementAtOrNull( |
| int.parse(id.substring('from_package/'.length)) - 1, |
| ); |
| request = profileMap == null |
| ? null |
| : _createHttpProfileRequestFromProfileMap(profileMap, ref: false); |
| } else { |
| request = HttpProfiler.getHttpProfileRequest(id)?.toJson(ref: false); |
| } |
| |
| if (request == null) { |
| throw "Unable to find request with id: '$id'"; |
| } |
| return json.encode(request); |
| } |
| |
| String _socketProfilingEnabled(Map<String, String> parameters) { |
| const String kEnabled = 'enabled'; |
| if (parameters.containsKey(kEnabled)) { |
| final enable = parameters[kEnabled]!.toLowerCase(); |
| if (enable != 'true' && enable != 'false') { |
| throw _invalidArgument(kEnabled, enable); |
| } |
| enable == 'true' ? _SocketProfile.start() : _SocketProfile.pause(); |
| } |
| return json.encode({ |
| 'type': 'SocketProfilingState', |
| 'enabled': _SocketProfile.enableSocketProfiling, |
| }); |
| } |
| |
| abstract class _SocketProfile { |
| static const _kType = 'SocketProfile'; |
| static set enableSocketProfiling(bool enabled) { |
| if (enabled != _enableSocketProfiling) { |
| postEvent('SocketProfilingStateChange', { |
| 'isolateId': Service.getIsolateID(Isolate.current), |
| 'enabled': enabled, |
| }); |
| _enableSocketProfiling = enabled; |
| } |
| } |
| |
| static bool get enableSocketProfiling => _enableSocketProfiling; |
| |
| static bool _enableSocketProfiling = false; |
| static Map<String, _SocketStatistic> _idToSocketStatistic = {}; |
| |
| static String toJson() => json.encode({ |
| 'type': _kType, |
| 'sockets': _idToSocketStatistic.values.map((f) => f.toMap()).toList(), |
| }); |
| |
| static void collectNewSocket( |
| int id, |
| String type, |
| InternetAddress addr, |
| int port, |
| ) { |
| if (!_enableSocketProfiling) { |
| return; |
| } |
| // TODO(srawlins): Assert that `_idToSocketStatistic` does not contain |
| // `id.toString()`? |
| final address = |
| (addr.type == InternetAddress.anyIPv6 || |
| addr.type == InternetAddress.loopbackIPv6) |
| ? '[${addr.address}]' |
| : addr.address; |
| _idToSocketStatistic[id.toString()] = _SocketStatistic( |
| id.toString(), |
| startTime: Timeline.now, |
| socketType: type, |
| address: address, |
| port: port, |
| ); |
| } |
| |
| static void collectStatistic( |
| int id, |
| _SocketProfileType type, [ |
| Object? object, |
| ]) { |
| final idKey = id.toString(); |
| if (!_enableSocketProfiling) { |
| return; |
| } |
| // Skip any socket that started before `_enableSocketProfiling` was turned |
| // on. |
| final stats = _idToSocketStatistic[idKey]; |
| if (stats == null) return; |
| switch (type) { |
| case _SocketProfileType.endTime: |
| stats.endTime = Timeline.now; |
| break; |
| case _SocketProfileType.readBytes: |
| if (object == null) return; |
| stats.readBytes += object as int; |
| stats.lastReadTime = Timeline.now; |
| break; |
| case _SocketProfileType.writeBytes: |
| if (object == null) return; |
| stats.writeBytes += object as int; |
| stats.lastWriteTime = Timeline.now; |
| break; |
| case _SocketProfileType.startTime: |
| case _SocketProfileType.socketType: |
| case _SocketProfileType.address: |
| case _SocketProfileType.port: |
| throw ArgumentError( |
| 'The "${type}" type can only be set on initialization', |
| ); |
| } |
| } |
| |
| static String start() { |
| enableSocketProfiling = true; |
| return _success(); |
| } |
| |
| static String pause() { |
| enableSocketProfiling = false; |
| return _success(); |
| } |
| |
| // clear the storage if _idToSocketStatistic has been initialized. |
| static String clear() { |
| _idToSocketStatistic.clear(); |
| return _success(); |
| } |
| } |
| |
| /// The [_SocketProfileType] is used as a parameter for |
| /// [_SocketProfile.collectStatistic] to determine the type of statistic. |
| enum _SocketProfileType { |
| startTime, |
| endTime, |
| address, |
| port, |
| socketType, |
| readBytes, |
| writeBytes, |
| } |
| |
| /// Socket statistic |
| class _SocketStatistic { |
| final String id; |
| final int startTime; |
| int? endTime; |
| final String address; |
| final int port; |
| final String socketType; |
| int readBytes = 0; |
| int writeBytes = 0; |
| int? lastWriteTime; |
| int? lastReadTime; |
| |
| _SocketStatistic( |
| this.id, { |
| required this.startTime, |
| required this.socketType, |
| required this.address, |
| required this.port, |
| }); |
| |
| Map<String, dynamic> toMap() { |
| final map = <String, Object>{ |
| 'id': id, |
| 'startTime': startTime, |
| 'address': address, |
| 'port': port, |
| 'socketType': socketType, |
| }; |
| // TODO(srawlins): Replace with null-aware elements. |
| _setIfNotNull(map, 'endTime', endTime); |
| _setIfNotNull(map, 'readBytes', readBytes); |
| _setIfNotNull(map, 'writeBytes', writeBytes); |
| _setIfNotNull(map, 'lastWriteTime', lastWriteTime); |
| _setIfNotNull(map, 'lastReadTime', lastReadTime); |
| return map; |
| } |
| |
| void _setIfNotNull(Map<String, dynamic> json, String key, Object? value) { |
| if (value == null) return; |
| json[key] = value; |
| } |
| } |