mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 08:24:39 +00:00
[analysis_server] Record timings for recent LSP requests and show in server diagnostics
Change-Id: I1bfb03faa9ce3240c700c93cb4ab677dc1aff520 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243820 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
This commit is contained in:
parent
7d6216940f
commit
5f15aefcab
|
@ -285,3 +285,14 @@ class LspJsonHandler<T> {
|
|||
abstract class ToJsonable {
|
||||
Object toJson();
|
||||
}
|
||||
|
||||
extension IncomingMessageExtension on IncomingMessage {
|
||||
/// Returns the amount of time (in milliseconds) since the client sent this
|
||||
/// request or `null` if the client did not provide [clientRequestTime].
|
||||
int? get timeSinceRequest {
|
||||
var clientRequestTime = this.clientRequestTime;
|
||||
return clientRequestTime != null
|
||||
? DateTime.now().millisecondsSinceEpoch - clientRequestTime
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@ import 'package:analysis_server/src/plugin/plugin_manager.dart';
|
|||
import 'package:analysis_server/src/plugin/plugin_watcher.dart';
|
||||
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
|
||||
import 'package:analysis_server/src/server/diagnostic_server.dart';
|
||||
import 'package:analysis_server/src/server/performance.dart';
|
||||
import 'package:analysis_server/src/services/completion/completion_performance.dart';
|
||||
import 'package:analysis_server/src/services/completion/dart/documentation_cache.dart';
|
||||
import 'package:analysis_server/src/services/correction/namespace.dart';
|
||||
import 'package:analysis_server/src/services/pub/pub_api.dart';
|
||||
|
@ -124,6 +126,9 @@ abstract class AbstractAnalysisServer {
|
|||
/// Performance information before initial analysis is complete.
|
||||
final ServerPerformance performanceDuringStartup = ServerPerformance();
|
||||
|
||||
/// Performance about recent requests.
|
||||
final ServerRecentPerformance recentPerformance = ServerRecentPerformance();
|
||||
|
||||
RequestStatisticsHelper? requestStatistics;
|
||||
|
||||
PerformanceLog? analysisPerformanceLogger;
|
||||
|
@ -543,3 +548,18 @@ abstract class AbstractAnalysisServer {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class ServerRecentPerformance {
|
||||
/// The maximum number of performance measurements to keep.
|
||||
static const int performanceListMaxLength = 50;
|
||||
|
||||
/// A list of code completion performance measurements for the latest
|
||||
/// completion operation up to [performanceListMaxLength] measurements.
|
||||
final RecentBuffer<CompletionPerformance> completion =
|
||||
RecentBuffer<CompletionPerformance>(performanceListMaxLength);
|
||||
|
||||
/// A [RecentBuffer] for performance information about the most recent
|
||||
/// requests.
|
||||
final RecentBuffer<RequestPerformance> requests =
|
||||
RecentBuffer(performanceListMaxLength);
|
||||
}
|
||||
|
|
|
@ -93,13 +93,13 @@ class CompletionGetSuggestionsHandler extends CompletionGetSuggestions2Handler {
|
|||
}
|
||||
|
||||
final completionPerformance = CompletionPerformance(
|
||||
operation: performance,
|
||||
performance: performance,
|
||||
path: file,
|
||||
requestLatency: requestLatency,
|
||||
content: resolvedUnit.content,
|
||||
offset: offset,
|
||||
);
|
||||
server.completionState.performanceList.add(completionPerformance);
|
||||
server.recentPerformance.completion.add(completionPerformance);
|
||||
|
||||
var declarationsTracker = server.declarationsTracker;
|
||||
if (declarationsTracker == null) {
|
||||
|
|
|
@ -176,13 +176,13 @@ class CompletionGetSuggestions2Handler extends CompletionHandler
|
|||
}
|
||||
|
||||
final completionPerformance = CompletionPerformance(
|
||||
operation: performance,
|
||||
performance: performance,
|
||||
path: file,
|
||||
requestLatency: requestLatency,
|
||||
content: resolvedUnit.content,
|
||||
offset: offset,
|
||||
);
|
||||
server.completionState.performanceList.add(completionPerformance);
|
||||
server.recentPerformance.completion.add(completionPerformance);
|
||||
|
||||
var analysisSession = resolvedUnit.analysisSession;
|
||||
var enclosingNode =
|
||||
|
|
|
@ -115,14 +115,14 @@ class CompletionHandler extends MessageHandler<CompletionParams, CompletionList>
|
|||
'request',
|
||||
(performance) async {
|
||||
final thisPerformance = CompletionPerformance(
|
||||
operation: performance,
|
||||
performance: performance,
|
||||
path: result.path,
|
||||
requestLatency: requestLatency,
|
||||
content: result.content,
|
||||
offset: offset,
|
||||
);
|
||||
completionPerformance = thisPerformance;
|
||||
server.performanceStats.completion.add(thisPerformance);
|
||||
server.recentPerformance.completion.add(thisPerformance);
|
||||
|
||||
// `await` required for `performance.runAsync` to count time.
|
||||
return await _getServerDartItems(
|
||||
|
|
|
@ -183,7 +183,7 @@ abstract class MessageHandler<P, R>
|
|||
|
||||
final params =
|
||||
paramsJson != null ? jsonHandler.convertParams(paramsJson) : null as P;
|
||||
final messageInfo = MessageInfo(message.clientRequestTime);
|
||||
final messageInfo = MessageInfo(timeSinceRequest: message.timeSinceRequest);
|
||||
return handle(params, messageInfo, token);
|
||||
}
|
||||
}
|
||||
|
@ -191,18 +191,11 @@ abstract class MessageHandler<P, R>
|
|||
/// Additional information about an incoming message (request or notification)
|
||||
/// provided to a handler.
|
||||
class MessageInfo {
|
||||
final int? clientRequestTime;
|
||||
|
||||
MessageInfo(this.clientRequestTime);
|
||||
|
||||
/// Returns the amount of time (in milliseconds) since the client sent this
|
||||
/// request or `null` if the client did not provide [clientRequestTime].
|
||||
int? get timeSinceRequest {
|
||||
var clientRequestTime = this.clientRequestTime;
|
||||
return clientRequestTime != null
|
||||
? DateTime.now().millisecondsSinceEpoch - clientRequestTime
|
||||
: null;
|
||||
}
|
||||
final int? timeSinceRequest;
|
||||
|
||||
MessageInfo({this.timeSinceRequest});
|
||||
}
|
||||
|
||||
/// A message handler that handles all messages for a given server state.
|
||||
|
|
|
@ -9,7 +9,6 @@ import 'package:analysis_server/lsp_protocol/protocol_generated.dart';
|
|||
import 'package:analysis_server/lsp_protocol/protocol_special.dart';
|
||||
import 'package:analysis_server/src/analysis_server.dart';
|
||||
import 'package:analysis_server/src/analysis_server_abstract.dart';
|
||||
import 'package:analysis_server/src/collections.dart';
|
||||
import 'package:analysis_server/src/computer/computer_closingLabels.dart';
|
||||
import 'package:analysis_server/src/computer/computer_outline.dart';
|
||||
import 'package:analysis_server/src/context_manager.dart';
|
||||
|
@ -30,9 +29,7 @@ import 'package:analysis_server/src/protocol_server.dart' as protocol;
|
|||
import 'package:analysis_server/src/server/crash_reporting_attachments.dart';
|
||||
import 'package:analysis_server/src/server/diagnostic_server.dart';
|
||||
import 'package:analysis_server/src/server/error_notifier.dart';
|
||||
import 'package:analysis_server/src/services/completion/completion_performance.dart'
|
||||
show CompletionPerformance;
|
||||
import 'package:analysis_server/src/services/completion/completion_state.dart';
|
||||
import 'package:analysis_server/src/server/performance.dart';
|
||||
import 'package:analysis_server/src/services/refactoring/refactoring.dart';
|
||||
import 'package:analysis_server/src/utilities/process.dart';
|
||||
import 'package:analyzer/dart/analysis/context_locator.dart';
|
||||
|
@ -47,6 +44,7 @@ import 'package:analyzer/src/dart/analysis/driver.dart' as analysis;
|
|||
import 'package:analyzer/src/dart/analysis/status.dart' as analysis;
|
||||
import 'package:analyzer/src/generated/sdk.dart';
|
||||
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
|
||||
import 'package:analyzer/src/util/performance/operation_performance.dart';
|
||||
import 'package:analyzer_plugin/protocol/protocol_common.dart' as plugin;
|
||||
import 'package:analyzer_plugin/protocol/protocol_generated.dart' as plugin;
|
||||
import 'package:analyzer_plugin/src/protocol/protocol_internal.dart' as plugin;
|
||||
|
@ -99,8 +97,6 @@ class LspAnalysisServer extends AbstractAnalysisServer {
|
|||
ServerCapabilities? capabilities;
|
||||
late ServerCapabilitiesComputer capabilitiesComputer;
|
||||
|
||||
LspPerformance performanceStats = LspPerformance();
|
||||
|
||||
/// Whether or not the server is controlling the shutdown and will exit
|
||||
/// automatically.
|
||||
bool willExit = false;
|
||||
|
@ -355,15 +351,25 @@ class LspAnalysisServer extends AbstractAnalysisServer {
|
|||
if (message is ResponseMessage) {
|
||||
handleClientResponse(message);
|
||||
} else if (message is RequestMessage) {
|
||||
final result = await messageHandler.handleMessage(message);
|
||||
if (result.isError) {
|
||||
sendErrorResponse(message, result.error);
|
||||
} else {
|
||||
channel.sendResponse(ResponseMessage(
|
||||
id: message.id,
|
||||
result: result.result,
|
||||
jsonrpc: jsonRpcVersion));
|
||||
}
|
||||
// Record performance information for the request.
|
||||
final performance = OperationPerformanceImpl('<root>');
|
||||
await performance.runAsync('request', (performance) async {
|
||||
final requestPerformance = RequestPerformance(
|
||||
operation: message.method.toString(),
|
||||
performance: performance,
|
||||
requestLatency: message.timeSinceRequest,
|
||||
);
|
||||
recentPerformance.requests.add(requestPerformance);
|
||||
final result = await messageHandler.handleMessage(message);
|
||||
if (result.isError) {
|
||||
sendErrorResponse(message, result.error);
|
||||
} else {
|
||||
channel.sendResponse(ResponseMessage(
|
||||
id: message.id,
|
||||
result: result.result,
|
||||
jsonrpc: jsonRpcVersion));
|
||||
}
|
||||
});
|
||||
} else if (message is NotificationMessage) {
|
||||
final result = await messageHandler.handleMessage(message);
|
||||
if (result.isError) {
|
||||
|
@ -857,14 +863,6 @@ class LspInitializationOptions {
|
|||
flutterOutline = options != null && options['flutterOutline'] == true;
|
||||
}
|
||||
|
||||
class LspPerformance {
|
||||
/// A list of code completion performance measurements for the latest
|
||||
/// completion operation up to [performanceListMaxLength] measurements.
|
||||
final RecentBuffer<CompletionPerformance> completion =
|
||||
RecentBuffer<CompletionPerformance>(
|
||||
CompletionState.performanceListMaxLength);
|
||||
}
|
||||
|
||||
class LspServerContextManagerCallbacks extends ContextManagerCallbacks {
|
||||
// TODO(dantup): Lots of copy/paste from the Analysis Server one here.
|
||||
|
||||
|
|
19
pkg/analysis_server/lib/src/server/performance.dart
Normal file
19
pkg/analysis_server/lib/src/server/performance.dart
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright (c) 2022, 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 'package:analyzer/src/util/performance/operation_performance.dart';
|
||||
|
||||
class RequestPerformance {
|
||||
static var _nextId = 1;
|
||||
final int id;
|
||||
final OperationPerformance performance;
|
||||
final int? requestLatency;
|
||||
final String operation;
|
||||
|
||||
RequestPerformance({
|
||||
required this.operation,
|
||||
required this.performance,
|
||||
this.requestLatency,
|
||||
}) : id = _nextId++;
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
// 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 'package:analyzer/src/util/performance/operation_performance.dart';
|
||||
import 'package:analysis_server/src/server/performance.dart';
|
||||
|
||||
/// Compute a string representing a code completion operation at the
|
||||
/// given source and location.
|
||||
|
@ -34,24 +34,20 @@ String _computeCompletionSnippet(String contents, int offset) {
|
|||
}
|
||||
|
||||
/// Overall performance of a code completion operation.
|
||||
class CompletionPerformance {
|
||||
static var _nextId = 1;
|
||||
final int id;
|
||||
final OperationPerformance operation;
|
||||
class CompletionPerformance extends RequestPerformance {
|
||||
final String path;
|
||||
final String snippet;
|
||||
final int? requestLatency;
|
||||
int? computedSuggestionCount;
|
||||
int? transmittedSuggestionCount;
|
||||
|
||||
CompletionPerformance({
|
||||
required this.operation,
|
||||
required super.performance,
|
||||
required this.path,
|
||||
this.requestLatency,
|
||||
super.requestLatency,
|
||||
required String content,
|
||||
required int offset,
|
||||
}) : id = _nextId++,
|
||||
snippet = _computeCompletionSnippet(content, offset);
|
||||
}) : snippet = _computeCompletionSnippet(content, offset),
|
||||
super(operation: 'Completion');
|
||||
|
||||
String get computedSuggestionCountStr {
|
||||
if (computedSuggestionCount == null) return '';
|
||||
|
@ -59,7 +55,7 @@ class CompletionPerformance {
|
|||
}
|
||||
|
||||
int get elapsedInMilliseconds {
|
||||
return operation.elapsed.inMilliseconds;
|
||||
return performance.elapsed.inMilliseconds;
|
||||
}
|
||||
|
||||
String get transmittedSuggestionCountStr {
|
||||
|
|
|
@ -3,14 +3,9 @@
|
|||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:analysis_server/protocol/protocol_generated.dart';
|
||||
import 'package:analysis_server/src/collections.dart';
|
||||
import 'package:analysis_server/src/services/completion/completion_performance.dart';
|
||||
import 'package:analysis_server/src/services/completion/dart/completion_manager.dart';
|
||||
|
||||
class CompletionState {
|
||||
/// The maximum number of performance measurements to keep.
|
||||
static const int performanceListMaxLength = 50;
|
||||
|
||||
/// The time budget for a completion request.
|
||||
Duration budgetDuration = CompletionBudget.defaultDuration;
|
||||
|
||||
|
@ -20,11 +15,6 @@ class CompletionState {
|
|||
/// The next completion response id.
|
||||
int nextCompletionId = 0;
|
||||
|
||||
/// A list of code completion performance measurements for the latest
|
||||
/// completion operation up to [performanceListMaxLength] measurements.
|
||||
final RecentBuffer<CompletionPerformance> performanceList =
|
||||
RecentBuffer<CompletionPerformance>(performanceListMaxLength);
|
||||
|
||||
/// The current request being processed or `null` if none.
|
||||
DartCompletionRequest? currentRequest;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:analysis_server/src/lsp/lsp_analysis_server.dart'
|
|||
show LspAnalysisServer;
|
||||
import 'package:analysis_server/src/plugin/plugin_manager.dart';
|
||||
import 'package:analysis_server/src/server/http_server.dart';
|
||||
import 'package:analysis_server/src/server/performance.dart';
|
||||
import 'package:analysis_server/src/services/completion/completion_performance.dart';
|
||||
import 'package:analysis_server/src/socket_server.dart';
|
||||
import 'package:analysis_server/src/status/ast_writer.dart';
|
||||
|
@ -156,136 +157,6 @@ String writeOption(String name, dynamic value) {
|
|||
return '$name: <code>$value</code><br> ';
|
||||
}
|
||||
|
||||
abstract class AbstractCompletionPage extends DiagnosticPageWithNav {
|
||||
AbstractCompletionPage(DiagnosticsSite site)
|
||||
: super(site, 'completion', 'Code Completion',
|
||||
description: 'Latency statistics for code completion.');
|
||||
|
||||
path.Context get pathContext;
|
||||
|
||||
List<CompletionPerformance> get performanceItems;
|
||||
|
||||
@override
|
||||
Future generateContent(Map<String, String> params) async {
|
||||
var completions = performanceItems;
|
||||
|
||||
if (completions.isEmpty) {
|
||||
blankslate('No completions recorded.');
|
||||
return;
|
||||
}
|
||||
|
||||
var fastCount =
|
||||
completions.where((c) => c.elapsedInMilliseconds <= 100).length;
|
||||
p('${completions.length} results; ${printPercentage(fastCount / completions.length)} within 100ms.');
|
||||
|
||||
// draw a chart
|
||||
buf.writeln(
|
||||
'<div id="chart-div" style="width: 700px; height: 300px;"></div>');
|
||||
var rowData = StringBuffer();
|
||||
for (var i = completions.length - 1; i >= 0; i--) {
|
||||
if (rowData.isNotEmpty) {
|
||||
rowData.write(',');
|
||||
}
|
||||
var latency = completions[i].requestLatency ?? 0;
|
||||
var completionTime = completions[i].elapsedInMilliseconds;
|
||||
// label, latency, time
|
||||
// [' ', 21.0, 101.5]
|
||||
rowData.write("[' ', $latency, $completionTime]");
|
||||
}
|
||||
buf.writeln('''
|
||||
<script type="text/javascript">
|
||||
google.charts.load('current', {'packages':['bar']});
|
||||
google.charts.setOnLoadCallback(drawChart);
|
||||
function drawChart() {
|
||||
var data = google.visualization.arrayToDataTable([
|
||||
[ 'Completion', 'Latency', 'Time' ],
|
||||
$rowData
|
||||
]);
|
||||
var options = {
|
||||
bars: 'vertical',
|
||||
vAxis: {format: 'decimal'},
|
||||
height: 300,
|
||||
isStacked: true,
|
||||
series: {
|
||||
0: { color: '#C0C0C0' },
|
||||
1: { color: '#4285f4' },
|
||||
}
|
||||
};
|
||||
var chart = new google.charts.Bar(document.getElementById('chart-div'));
|
||||
chart.draw(data, google.charts.Bar.convertOptions(options));
|
||||
}
|
||||
</script>
|
||||
''');
|
||||
|
||||
// emit the data as a table
|
||||
buf.writeln('<table>');
|
||||
buf.writeln(
|
||||
'<tr><th>Time</th><th>Computed Results</th><th>Transmitted Results</th><th>Source</th><th>Snippet</th></tr>');
|
||||
for (var completion in completions) {
|
||||
var shortName = pathContext.basename(completion.path);
|
||||
buf.writeln('<tr>'
|
||||
'<td class="pre right"><a href="/timing?id=${completion.id}">'
|
||||
'${_formatTiming(completion)}'
|
||||
'</a></td>'
|
||||
'<td class="right">${completion.computedSuggestionCountStr}</td>'
|
||||
'<td class="right">${completion.transmittedSuggestionCountStr}</td>'
|
||||
'<td>${escape(shortName)}</td>'
|
||||
'<td><code>${escape(completion.snippet)}</code></td>'
|
||||
'</tr>');
|
||||
}
|
||||
buf.writeln('</table>');
|
||||
}
|
||||
|
||||
String _formatTiming(CompletionPerformance completion) {
|
||||
var buffer = StringBuffer();
|
||||
buffer.write(printMilliseconds(completion.elapsedInMilliseconds));
|
||||
|
||||
var latency = completion.requestLatency;
|
||||
if (latency != null) {
|
||||
buffer
|
||||
..write(' <small class="subtle" title="client-to-server latency">(+ ')
|
||||
..write(printMilliseconds(latency))
|
||||
..write(')</small>');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AbstractCompletionTimingPage extends DiagnosticPageWithNav {
|
||||
AbstractCompletionTimingPage(DiagnosticsSite site)
|
||||
: super(site, 'timing', 'Timing', description: 'Timing statistics.');
|
||||
|
||||
path.Context get pathContext;
|
||||
|
||||
List<CompletionPerformance> get performanceItems;
|
||||
|
||||
@override
|
||||
bool get showInNav => false;
|
||||
|
||||
@override
|
||||
Future generateContent(Map<String, String> params) async {
|
||||
var id = int.parse(params['id'] ?? '');
|
||||
var completionInfo =
|
||||
performanceItems.firstWhereOrNull((info) => info.id == id);
|
||||
|
||||
if (completionInfo == null) {
|
||||
blankslate('Unable to find completion data for $id. '
|
||||
'Perhaps newer completion requests have pushed it out of the buffer?');
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = StringBuffer();
|
||||
completionInfo.operation.write(buffer: buffer);
|
||||
pre(() {
|
||||
buf.write('<code>');
|
||||
buf.write(escape('$buffer'));
|
||||
buf.writeln('</code>');
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class AstPage extends DiagnosticPageWithNav {
|
||||
String? _description;
|
||||
|
||||
|
@ -416,32 +287,66 @@ class CommunicationsPage extends DiagnosticPageWithNav {
|
|||
}
|
||||
}
|
||||
|
||||
class CompletionPage extends AbstractCompletionPage {
|
||||
@override
|
||||
AnalysisServer server;
|
||||
class CompletionPage extends DiagnosticPageWithNav with PerformanceChartMixin {
|
||||
CompletionPage(DiagnosticsSite site)
|
||||
: super(site, 'completion', 'Code Completion',
|
||||
description: 'Latency statistics for code completion.');
|
||||
|
||||
CompletionPage(super.site, this.server);
|
||||
|
||||
@override
|
||||
path.Context get pathContext => server.resourceProvider.pathContext;
|
||||
|
||||
@override
|
||||
List<CompletionPerformance> get performanceItems =>
|
||||
server.completionState.performanceList.items.toList();
|
||||
}
|
||||
|
||||
class CompletionTimingPage extends AbstractCompletionTimingPage {
|
||||
@override
|
||||
AnalysisServer server;
|
||||
|
||||
CompletionTimingPage(super.site, this.server);
|
||||
server.recentPerformance.completion.items.toList();
|
||||
|
||||
@override
|
||||
path.Context get pathContext => server.resourceProvider.pathContext;
|
||||
Future generateContent(Map<String, String> params) async {
|
||||
var completions = performanceItems;
|
||||
|
||||
@override
|
||||
List<CompletionPerformance> get performanceItems =>
|
||||
server.completionState.performanceList.items.toList();
|
||||
if (completions.isEmpty) {
|
||||
blankslate('No completions recorded.');
|
||||
return;
|
||||
}
|
||||
|
||||
var fastCount =
|
||||
completions.where((c) => c.elapsedInMilliseconds <= 100).length;
|
||||
p('${completions.length} results; ${printPercentage(fastCount / completions.length)} within 100ms.');
|
||||
|
||||
drawChart(completions);
|
||||
|
||||
// emit the data as a table
|
||||
buf.writeln('<table>');
|
||||
buf.writeln(
|
||||
'<tr><th>Time</th><th>Computed Results</th><th>Transmitted Results</th><th>Source</th><th>Snippet</th></tr>');
|
||||
for (var completion in completions) {
|
||||
var shortName = pathContext.basename(completion.path);
|
||||
buf.writeln(
|
||||
'<tr>'
|
||||
'<td class="pre right"><a href="/timing?id=${completion.id}&kind=completion">'
|
||||
'${_formatTiming(completion)}'
|
||||
'</a></td>'
|
||||
'<td class="right">${completion.computedSuggestionCountStr}</td>'
|
||||
'<td class="right">${completion.transmittedSuggestionCountStr}</td>'
|
||||
'<td>${escape(shortName)}</td>'
|
||||
'<td><code>${escape(completion.snippet)}</code></td>'
|
||||
'</tr>',
|
||||
);
|
||||
}
|
||||
buf.writeln('</table>');
|
||||
}
|
||||
|
||||
String _formatTiming(CompletionPerformance completion) {
|
||||
var buffer = StringBuffer();
|
||||
buffer.write(printMilliseconds(completion.elapsedInMilliseconds));
|
||||
|
||||
var latency = completion.requestLatency;
|
||||
if (latency != null) {
|
||||
buffer
|
||||
..write(' <small class="subtle" title="client-to-server latency">(+ ')
|
||||
..write(printMilliseconds(latency))
|
||||
..write(')</small>');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class ContentsPage extends DiagnosticPageWithNav {
|
||||
|
@ -844,15 +749,13 @@ class DiagnosticsSite extends Site implements AbstractGetHandler {
|
|||
if (server != null) {
|
||||
pages.add(PluginsPage(this, server));
|
||||
}
|
||||
pages.add(CompletionPage(this));
|
||||
if (server is AnalysisServer) {
|
||||
pages.add(CompletionPage(this, server));
|
||||
pages.add(CompletionTimingPage(this, server));
|
||||
pages.add(SubscriptionsPage(this, server));
|
||||
} else if (server is LspAnalysisServer) {
|
||||
pages.add(LspCompletionPage(this, server));
|
||||
pages.add(LspCompletionTimingPage(this, server));
|
||||
pages.add(LspCapabilitiesPage(this, server));
|
||||
}
|
||||
pages.add(TimingPage(this));
|
||||
|
||||
var profiler = ProcessProfiler.getProfilerForPlatform();
|
||||
if (profiler != null) {
|
||||
|
@ -1110,34 +1013,6 @@ class LspCapabilitiesPage extends DiagnosticPageWithNav {
|
|||
// }
|
||||
// }
|
||||
|
||||
class LspCompletionPage extends AbstractCompletionPage {
|
||||
@override
|
||||
LspAnalysisServer server;
|
||||
|
||||
LspCompletionPage(super.site, this.server);
|
||||
|
||||
@override
|
||||
path.Context get pathContext => server.resourceProvider.pathContext;
|
||||
|
||||
@override
|
||||
List<CompletionPerformance> get performanceItems =>
|
||||
server.performanceStats.completion.items.toList();
|
||||
}
|
||||
|
||||
class LspCompletionTimingPage extends AbstractCompletionTimingPage {
|
||||
@override
|
||||
LspAnalysisServer server;
|
||||
|
||||
LspCompletionTimingPage(super.site, this.server);
|
||||
|
||||
@override
|
||||
path.Context get pathContext => server.resourceProvider.pathContext;
|
||||
|
||||
@override
|
||||
List<CompletionPerformance> get performanceItems =>
|
||||
server.performanceStats.completion.items.toList();
|
||||
}
|
||||
|
||||
class MemoryAndCpuPage extends DiagnosticPageWithNav {
|
||||
final ProcessProfiler profiler;
|
||||
|
||||
|
@ -1255,7 +1130,7 @@ class PluginsPage extends DiagnosticPageWithNav {
|
|||
var requestName = entry.key;
|
||||
var data = entry.value;
|
||||
// TODO(brianwilkerson) Consider displaying these times as a graph,
|
||||
// similar to the one in AbstractCompletionPage.generateContent.
|
||||
// similar to the one in CompletionPage.generateContent.
|
||||
var buffer = StringBuffer();
|
||||
buffer.write(requestName);
|
||||
buffer.write(' ');
|
||||
|
@ -1356,3 +1231,84 @@ class SubscriptionsPage extends DiagnosticPageWithNav {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
class TimingPage extends DiagnosticPageWithNav with PerformanceChartMixin {
|
||||
TimingPage(DiagnosticsSite site)
|
||||
: super(site, 'timing', 'Timing', description: 'Timing statistics.');
|
||||
|
||||
@override
|
||||
Future generateContent(Map<String, String> params) async {
|
||||
var kind = params['kind'];
|
||||
|
||||
List<RequestPerformance> items;
|
||||
if (kind == 'completion') {
|
||||
items = server.recentPerformance.completion.items.toList();
|
||||
} else {
|
||||
items = server.recentPerformance.requests.items.toList();
|
||||
}
|
||||
|
||||
var id = int.tryParse(params['id'] ?? '');
|
||||
if (id == null) {
|
||||
return _generateList(items);
|
||||
} else {
|
||||
return _generateDetails(id, items);
|
||||
}
|
||||
}
|
||||
|
||||
String _formatTiming(RequestPerformance item) {
|
||||
var buffer = StringBuffer();
|
||||
buffer.write(printMilliseconds(item.performance.elapsed.inMilliseconds));
|
||||
|
||||
var latency = item.requestLatency;
|
||||
if (latency != null) {
|
||||
buffer
|
||||
..write(' <small class="subtle" title="client-to-server latency">(+ ')
|
||||
..write(printMilliseconds(latency))
|
||||
..write(')</small>');
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
void _generateDetails(int id, List<RequestPerformance> items) {
|
||||
var item = items.firstWhereOrNull((info) => info.id == id);
|
||||
|
||||
if (item == null) {
|
||||
blankslate('Unable to find data for $id. '
|
||||
'Perhaps newer requests have pushed it out of the buffer?');
|
||||
return;
|
||||
}
|
||||
|
||||
var buffer = StringBuffer();
|
||||
item.performance.write(buffer: buffer);
|
||||
pre(() {
|
||||
buf.write('<code>');
|
||||
buf.write(escape('$buffer'));
|
||||
buf.writeln('</code>');
|
||||
});
|
||||
}
|
||||
|
||||
void _generateList(List<RequestPerformance> items) {
|
||||
if (items.isEmpty) {
|
||||
blankslate('No requests recorded.');
|
||||
return;
|
||||
}
|
||||
|
||||
drawChart(items);
|
||||
|
||||
// emit the data as a table
|
||||
buf.writeln('<table>');
|
||||
buf.writeln('<tr><th>Time</th><th>Request</th></tr>');
|
||||
for (var item in items) {
|
||||
buf.writeln(
|
||||
'<tr>'
|
||||
'<td class="pre right"><a href="/timing?id=${item.id}">'
|
||||
'${_formatTiming(item)}'
|
||||
'</a></td>'
|
||||
'<td>${escape(item.operation)}</td>'
|
||||
'</tr>',
|
||||
);
|
||||
}
|
||||
buf.writeln('</table>');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:analysis_server/src/server/performance.dart';
|
||||
|
||||
String escape(String? text) => text == null ? '' : htmlEscape.convert(text);
|
||||
|
||||
String printMilliseconds(int value) => '$value ms';
|
||||
|
@ -130,6 +132,48 @@ abstract class Page {
|
|||
}
|
||||
}
|
||||
|
||||
mixin PerformanceChartMixin on Page {
|
||||
void drawChart(List<RequestPerformance> items) {
|
||||
buf.writeln(
|
||||
'<div id="chart-div" style="width: 700px; height: 300px; padding-bottom: 30px;"></div>');
|
||||
var rowData = StringBuffer();
|
||||
for (var i = items.length - 1; i >= 0; i--) {
|
||||
if (rowData.isNotEmpty) {
|
||||
rowData.write(',');
|
||||
}
|
||||
var latency = items[i].requestLatency ?? 0;
|
||||
var time = items[i].performance.elapsed.inMilliseconds;
|
||||
// label, latency, time
|
||||
// [' ', 21.0, 101.5]
|
||||
rowData.write("[' ', $latency, $time]");
|
||||
}
|
||||
buf.writeln('''
|
||||
<script type="text/javascript">
|
||||
google.charts.load('current', {'packages':['bar']});
|
||||
google.charts.setOnLoadCallback(drawChart);
|
||||
function drawChart() {
|
||||
var data = google.visualization.arrayToDataTable([
|
||||
[ 'Request', 'Latency', 'Time' ],
|
||||
$rowData
|
||||
]);
|
||||
var options = {
|
||||
bars: 'vertical',
|
||||
vAxis: {format: 'decimal'},
|
||||
height: 300,
|
||||
isStacked: true,
|
||||
series: {
|
||||
0: { color: '#C0C0C0' },
|
||||
1: { color: '#4285f4' },
|
||||
}
|
||||
};
|
||||
var chart = new google.charts.Bar(document.getElementById('chart-div'));
|
||||
chart.draw(data, google.charts.Bar.convertOptions(options));
|
||||
}
|
||||
</script>
|
||||
''');
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains a collection of Pages.
|
||||
abstract class Site {
|
||||
final String title;
|
||||
|
|
|
@ -36,6 +36,19 @@ class ServerTest extends AbstractLspAnalysisServerTest {
|
|||
expect(server.performanceDuringStartup.latencyCount, isPositive);
|
||||
}
|
||||
|
||||
Future<void> test_capturesRequestPerformance() async {
|
||||
await initialize(includeClientRequestTime: true);
|
||||
await openFile(mainFileUri, '');
|
||||
await expectLater(
|
||||
getHover(mainFileUri, startOfDocPos),
|
||||
completes,
|
||||
);
|
||||
final performanceItems = server.recentPerformance.requests.items;
|
||||
final hoverItems = performanceItems.where(
|
||||
(item) => item.operation == Method.textDocument_hover.toString());
|
||||
expect(hoverItems, hasLength(1));
|
||||
}
|
||||
|
||||
Future<void> test_inconsistentStateError() async {
|
||||
await initialize(
|
||||
// Error is expected and checked below.
|
||||
|
|
Loading…
Reference in a new issue