// 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]. 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': requestProfile['requestBodyBytes']!, if (!ref && responseData['endTime'] != null) 'responseBody': requestProfile['responseBodyBytes']!, }; } @pragma('vm:entry-point', !const 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', !const 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); } static Future _serviceExtensionHandler( String method, Map 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), ...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 = await _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 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(); } Future _getHttpProfileRequest(Map parameters) async { if (!parameters.containsKey('id')) { throw _missingArgument('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); } if (request == null) { throw "Unable to find request with id: '$id'"; } return json.encode(request); } String _socketProfilingEnabled(Map 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 _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, ]) { final idKey = id.toString(); if (!_enableSocketProfiling) { return; } // Skip socket that started before _enableSocketProfiling turned on. if (!_idToSocketStatistic.containsKey(idKey) && type != _SocketProfileType.startTime) return; _SocketStatistic stats = _idToSocketStatistic[idKey] ??= _SocketStatistic(idKey); 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); final internetAddress = object as InternetAddress; stats.address = (internetAddress.type == InternetAddress.anyIPv6 || internetAddress.type == InternetAddress.loopbackIPv6) ? '[${internetAddress.address}]' : internetAddress.address; 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'); } } 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; int? startTime; int? endTime; String? address; int? port; String? socketType; int readBytes = 0; int writeBytes = 0; int? lastWriteTime; int? lastReadTime; _SocketStatistic(this.id); Map toMap() { final map = {'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 json, String key, Object? value) { if (value == null) return; json[key] = value; } }