blob: 279d3e83fb6d49d35d054038ef4507ae9f950b92 [file] [log] [blame] [edit]
// 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;
}
}