// 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'; int _nextSocketId = 0; /// 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 _createHttpProfileRequestFromProfileMap( Map requestProfile, { required bool ref, }) { 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', !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 _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), ...getHttpClientProfilingData() .where( (final Map p) => updatedSince == null || (p['_lastUpdateTime'] as int) >= updatedSince, ) .map( (p) => _createHttpProfileRequestFromProfileMap( p.cast(), 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, Object 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(); } String _getHttpProfileRequest(Map parameters) { final id = parameters['id']; if (id == null) { throw _missingArgument('id'); } final Map? request; if (id.startsWith('from_package/')) { final profileMap = getHttpClientProfilingData().elementAtOrNull( int.parse(id.substring('from_package/'.length)) - 1, ); request = profileMap == null ? null : _createHttpProfileRequestFromProfileMap( profileMap.cast(), 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, ) { if (!_enableSocketProfiling) { return; } final address = (addr.type == InternetAddress.anyIPv6 || addr.type == InternetAddress.loopbackIPv6) ? '[${addr.address}]' : addr.address; assert(!_idToSocketStatistic.containsKey(id)); _idToSocketStatistic[id] = _SocketStatistic( id, startTime: Timeline.now, socketType: type, address: address, port: port, ); } static void collectStatistic( int id, _SocketProfileType type, [ Object? object, ]) { if (!_enableSocketProfiling) { return; } // Skip any socket that started before `_enableSocketProfiling` was turned // on. final stats = _idToSocketStatistic[id]; 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 int 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 toMap() { final map = { 'id': id.toString(), '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 json, String key, Object? value) { if (value == null) return; json[key] = value; } }