blob: 7cdf2343b51f2ffff1165b6af253014955f0f8f8 [file] [log] [blame]
// 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.
import 'dart:io';
import 'package:analysis_server/protocol/protocol.dart';
import 'package:analysis_server/protocol/protocol_generated.dart';
import 'package:analysis_server/src/channel/byte_stream_channel.dart';
/// Helper for tracking request handling statistics.
///
/// All [DateTime] are local, not UTC.
class RequestStatisticsHelper {
final String _sdkVersion = Platform.version.split(' ').first;
final Map<String, _RequestStatistics> _statisticsMap = {};
/// The [StringSink] to which performance logger should copy its output.
late final _ServerLogStringSink _perfLoggerStringSink =
_ServerLogStringSink(this);
/// The channel to send 'server.log' notifications to.
ByteStreamServerChannel? _serverChannel;
/// Is `true` if the client subscribed for "server.log" notification.
bool _isNotificationSubscribed = false;
RequestStatisticsHelper();
/// Set whether the client subscribed for "server.log" notification.
set isNotificationSubscribed(bool value) {
_isNotificationSubscribed = value;
}
/// The [StringSink] to which performance logger should copy its output.
StringSink get perfLoggerStringSink => _perfLoggerStringSink;
/// The channel sets itself using this method.
set serverChannel(ByteStreamServerChannel serverChannel) {
_serverChannel = serverChannel;
}
/// Add a time marker item to the data associated with the [request].
void addItemTimeNow(Request request, String name) {
var id = request.id;
var stat = _statisticsMap[id];
if (stat != null) {
stat.items.add(
_RequestStatisticsItem(
name,
timeValue: DateTime.now(),
),
);
}
}
/// The new [request] was received. Record the time when the client sent it,
/// and the time when the server received it (now).
void addRequest(Request request) {
_logRequest(request);
var id = request.id;
var clientRequestMilliseconds = request.clientRequestTime;
if (clientRequestMilliseconds == null) {
return;
}
var clientRequestTime = DateTime.fromMillisecondsSinceEpoch(
clientRequestMilliseconds,
);
var serverRequestTime = DateTime.now();
_statisticsMap[id] = _RequestStatistics(
id,
request.method,
clientRequestTime,
serverRequestTime,
);
}
/// The server finished processing a request, and sends the [response].
/// Record the time when the response is about to be sent to the client.
void addResponse(Response response) {
if (!_isNotificationSubscribed) return;
if (_serverChannel == null) return;
var id = response.id;
var stat = _statisticsMap.remove(id);
if (stat != null) {
var responseTime = DateTime.now();
_sendLogEntry(ServerLogEntryKind.RESPONSE, stat.toJson(responseTime));
}
}
void logNotification(Notification notification) {
if (!_isNotificationSubscribed) return;
if (_serverChannel == null) return;
var event = notification.event;
// Don't log large and often notifications.
if (event == 'analysis.errors' ||
event == 'completion.availableSuggestions') {
return;
}
var params = notification.params;
if (params == null) {
return;
}
var map = <String, Object>{
'event': event,
};
if (event == 'analysis.highlights' ||
event == 'analysis.implemented' ||
event == 'analysis.navigation' ||
event == 'analysis.outline' ||
event == 'analysis.overrides') {
map['file'] = params['file'] as String;
}
if (event == 'server.status') {
var analysis = params['analysis'];
if (analysis is Map<String, Object?>) {
map['isAnalyzing'] = analysis['isAnalyzing'] as bool;
}
}
_sendLogEntry(ServerLogEntryKind.NOTIFICATION, map);
}
void _logRequest(Request request) {
if (!_isNotificationSubscribed) return;
if (_serverChannel == null) return;
var method = request.method;
var map = <String, Object>{
'id': request.id,
'method': method,
};
{
var clientRequestTime = request.clientRequestTime;
if (clientRequestTime != null) {
map['clientRequestTime'] = clientRequestTime;
}
}
if (method == 'analysis.updateContent') {
var filesMap = request.params['files'];
if (filesMap is Map<String, Object>) {
map['files'] = filesMap.keys.toList();
}
} else {
map = request.toJson();
}
_sendLogEntry(ServerLogEntryKind.REQUEST, map);
}
void _sendLogEntry(ServerLogEntryKind kind, Object data) {
if (!_isNotificationSubscribed) return;
var serverChannel = _serverChannel;
if (serverChannel == null) return;
serverChannel.sendNotification(
Notification(
'server.log',
<String, Object>{
'time': DateTime.now().millisecondsSinceEpoch,
'kind': kind.toJson(),
'data': data,
'sdkVersion': _sdkVersion,
},
),
);
}
}
class _RequestStatistics {
final String id;
final String method;
final DateTime clientRequestTime;
final DateTime serverRequestTime;
final List<_RequestStatisticsItem> items = [];
_RequestStatistics(
this.id,
this.method,
this.clientRequestTime,
this.serverRequestTime,
);
Map<String, Object> toJson(DateTime responseTime) {
var map = {
'id': id,
'method': method,
'clientRequestTime': clientRequestTime.millisecondsSinceEpoch,
'serverRequestTime': serverRequestTime.millisecondsSinceEpoch,
'responseTime': responseTime.millisecondsSinceEpoch,
};
if (items.isNotEmpty) {
map['items'] = items.map((item) => item.toJson()).toList();
}
return map;
}
}
class _RequestStatisticsItem {
final String name;
final DateTime? timeValue;
_RequestStatisticsItem(this.name, {this.timeValue});
Map<String, Object> toJson() {
final timeValue = this.timeValue;
if (timeValue != null) {
return {
'name': name,
'timeValue': timeValue.millisecondsSinceEpoch,
};
}
throw StateError('Unknown value: $name');
}
}
class _ServerLogStringSink implements StringSink {
final RequestStatisticsHelper helper;
_ServerLogStringSink(this.helper);
@override
void write(Object? obj) {
throw UnimplementedError();
}
@override
void writeAll(Iterable objects, [String separator = '']) {
throw UnimplementedError();
}
@override
void writeCharCode(int charCode) {
throw UnimplementedError();
}
@override
void writeln([Object? obj = '']) {
helper._sendLogEntry(ServerLogEntryKind.RAW, '$obj');
}
}