| // 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; |
| |
| const int _versionMajor = 1; |
| const int _versionMinor = 1; |
| |
| const String _tcpSocket = 'tcp'; |
| const String _udpSocket = 'udp'; |
| |
| @pragma('vm:entry-point', !const bool.fromEnvironment("dart.vm.product")) |
| abstract class _NetworkProfiling { |
| // Http relative RPCs |
| static const _kGetHttpEnableTimelineLogging = |
| 'ext.dart.io.getHttpEnableTimelineLogging'; |
| static const _kSetHttpEnableTimelineLogging = |
| 'ext.dart.io.setHttpEnableTimelineLogging'; |
| // Socket relative RPCs |
| static const _kClearSocketProfileRPC = 'ext.dart.io.clearSocketProfile'; |
| static const _kGetSocketProfileRPC = 'ext.dart.io.getSocketProfile'; |
| static const _kPauseSocketProfilingRPC = 'ext.dart.io.pauseSocketProfiling'; |
| static const _kStartSocketProfilingRPC = 'ext.dart.io.startSocketProfiling'; |
| |
| // 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') |
| static void _registerServiceExtension() { |
| registerExtension(_kGetHttpEnableTimelineLogging, _serviceExtensionHandler); |
| registerExtension(_kSetHttpEnableTimelineLogging, _serviceExtensionHandler); |
| registerExtension(_kGetSocketProfileRPC, _serviceExtensionHandler); |
| registerExtension(_kStartSocketProfilingRPC, _serviceExtensionHandler); |
| registerExtension(_kPauseSocketProfilingRPC, _serviceExtensionHandler); |
| registerExtension(_kClearSocketProfileRPC, _serviceExtensionHandler); |
| registerExtension(_kGetVersionRPC, _serviceExtensionHandler); |
| } |
| |
| static Future<ServiceExtensionResponse> _serviceExtensionHandler( |
| String method, Map<String, String> parameters) { |
| try { |
| String responseJson; |
| switch (method) { |
| case _kGetHttpEnableTimelineLogging: |
| responseJson = _getHttpEnableTimelineLogging(); |
| break; |
| case _kSetHttpEnableTimelineLogging: |
| responseJson = _setHttpEnableTimelineLogging(parameters); |
| break; |
| case _kGetSocketProfileRPC: |
| responseJson = _SocketProfile.toJson(); |
| break; |
| case _kStartSocketProfilingRPC: |
| responseJson = _SocketProfile.start(); |
| break; |
| case _kPauseSocketProfilingRPC: |
| responseJson = _SocketProfile.pause(); |
| break; |
| case _kClearSocketProfileRPC: |
| responseJson = _SocketProfile.clear(); |
| break; |
| case _kGetVersionRPC: |
| responseJson = getVersion(); |
| break; |
| default: |
| return Future.value(ServiceExtensionResponse.error( |
| ServiceExtensionResponse.extensionError, |
| 'Method $method does not exist')); |
| } |
| return Future.value(ServiceExtensionResponse.result(responseJson)); |
| } on dynamic catch (errorMessage) { |
| return Future.value(ServiceExtensionResponse.error( |
| ServiceExtensionResponse.invalidParams, errorMessage)); |
| } |
| } |
| |
| 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 kEnable = 'enable'; |
| if (!parameters.containsKey(kEnable)) { |
| throw _missingArgument(kEnable); |
| } |
| final enable = parameters[kEnable]!.toLowerCase(); |
| if (enable != 'true' && enable != 'false') { |
| throw _invalidArgument(kEnable, enable); |
| } |
| HttpClient.enableTimelineLogging = (enable == 'true'); |
| return _success(); |
| } |
| |
| abstract class _SocketProfile { |
| static const _kType = 'SocketProfile'; |
| static bool _enableSocketProfiling = false; |
| static Map<int, _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) { |
| _SocketProfile.collectStatistic(id, _SocketProfileType.startTime); |
| _SocketProfile.collectStatistic(id, _SocketProfileType.socketType, type); |
| _SocketProfile.collectStatistic(id, _SocketProfileType.address, addr); |
| _SocketProfile.collectStatistic(id, _SocketProfileType.port, port); |
| } |
| |
| static void collectStatistic(int id, _SocketProfileType type, |
| [dynamic object]) { |
| if (!_enableSocketProfiling) { |
| return; |
| } |
| // Skip socket that started before _enableSocketProfiling turned on. |
| if (!_idToSocketStatistic.containsKey(id) && |
| type != _SocketProfileType.startTime) return; |
| _SocketStatistic stats = _idToSocketStatistic[id] ??= _SocketStatistic(id); |
| switch (type) { |
| case _SocketProfileType.startTime: |
| stats.startTime = Timeline.now; |
| break; |
| case _SocketProfileType.endTime: |
| stats.endTime = Timeline.now; |
| break; |
| case _SocketProfileType.address: |
| assert(object is InternetAddress); |
| stats.address = (object as InternetAddress).toString(); |
| break; |
| case _SocketProfileType.port: |
| assert(object is int); |
| stats.port = object; |
| break; |
| case _SocketProfileType.socketType: |
| assert(object is String); |
| stats.socketType = object; |
| 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; |
| default: |
| throw ArgumentError('type ${type} does not exist'); |
| break; |
| } |
| } |
| |
| 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 int id; |
| int? startTime; |
| int? endTime; |
| String? address; |
| int? port; |
| String? socketType; |
| int readBytes = 0; |
| int writeBytes = 0; |
| int? lastWriteTime; |
| int? lastReadTime; |
| |
| _SocketStatistic(this.id); |
| |
| Map<String, dynamic> toMap() { |
| final map = <String, dynamic>{ |
| 'id': id, |
| }; |
| _setIfNotNull(map, 'startTime', startTime); |
| _setIfNotNull(map, 'endTime', endTime); |
| _setIfNotNull(map, 'address', address); |
| _setIfNotNull(map, 'port', port); |
| _setIfNotNull(map, 'socketType', socketType); |
| _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; |
| } |
| } |